From 11e29dffe71865f5a90a6700b95e483cfa7a3551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 21 Apr 2026 14:41:57 +0200 Subject: [PATCH 01/26] Drafting classes for better richtext handling --- .../DataHolders/TextLineSimpleTests.cs | 89 +++++++++++++++++ .../DataHolders/LineFragmentCollection.cs | 91 +++++++++++++++++ .../Integration/DataHolders/TextFragment.cs | 78 --------------- .../DataHolders/TextFragmentAdvanced.cs | 78 +++++++++++++++ ...n.cs => TextFragmentCollectionAdvanced.cs} | 0 .../TextFragmentCollectionSimple.cs | 2 + .../Integration/DataHolders/TextLine.cs | 17 ---- .../DataHolders/TextLineCollection.cs | 99 +++++++++++++++++++ .../Integration/DataHolders/TextLineSimple.cs | 54 +++++++++- .../DataHolders/TextLineVisualizer.cs | 45 +++++++++ .../Integration/TextLayoutEngine.RichText.cs | 2 + 11 files changed, 456 insertions(+), 99 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragment.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs rename src/EPPlus.Fonts.OpenType/Integration/DataHolders/{TextFragmentCollection.cs => TextFragmentCollectionAdvanced.cs} (100%) delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLine.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs new file mode 100644 index 000000000..205857ef2 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -0,0 +1,89 @@ +using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.TextShaping; +using EPPlus.Fonts.OpenType.Utils; +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Fonts.OpenType.Tests.DataHolders +{ + [TestClass] + public class TextLineSimpleTests + { + [TestMethod] + public void TestLineFragmentAbstraction() + { + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + + var fragments = GetTextFragments(); + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); + var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + + var line1 = wrappedLines[0]; + } + + List GetTextFragments() + { + List lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; + + var font1 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; + + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + var font6 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 24, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new() { font1, font2, font3, font4, font5, font6 }; + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + return fragments; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs new file mode 100644 index 000000000..2e22dec58 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration.DataHolders +{ + public class LineFragmentCollection : List + { + private string _text; + + public string FullText + { + get { return _text; } + internal set { RichTextSubstrings.Clear(); _text = value; } + } + + internal List RichTextSubstrings { get; private set; } + + + + public LineFragmentCollection(string originalText) + { + FullText = originalText; + } + + //Note: Array size never intended to be larger than 2 + List StartEndPerFragment = new List(); + + + /// + /// Logs start and end idx per fragment + /// Then adds fragment as regular list + /// + /// + public new void Add(LineFragment fragment) + { + var endIdx = FullText.Length - 1; + if (Count != 0) + { + StartEndPerFragment.Last()[1] = fragment.StartIdx; + } + + StartEndPerFragment.Add(new int[] { fragment.StartIdx, endIdx}); + base.Add(fragment); + } + + public string GetLineFragmentText(LineFragment rtFragment) + { + if (this.Contains(rtFragment) == false) + { + throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); + } + + if (string.IsNullOrEmpty(FullText)) + { + return FullText; + } + + var startIdx = rtFragment.StartIdx; + + var idxInLst = this.FindIndex(x => x == rtFragment); + if (idxInLst == this.Count - 1) + { + return FullText.Substring(startIdx, FullText.Length - startIdx); + } + else + { + var endIdx = this[idxInLst + 1].StartIdx; + return FullText.Substring(startIdx, endIdx - startIdx); + } + } + + internal void GenerateSubstrings() + { + RichTextSubstrings.Clear(); + + //for (int i = 0; i < StartEndPerFragment.Count; i++) + //{ + // var startIdx = StartEndPerFragment[i][0]; + // var endIdx = StartEndPerFragment[i][1]; + + // RichTextSubstrings.Add(FullText[1..5]); + //} + for (int i = 0; i < Count; i++) + { + RichTextSubstrings.Add(GetLineFragmentText(this[i])); + } + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragment.cs deleted file mode 100644 index 9627201cc..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragment.cs +++ /dev/null @@ -1,78 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; - -//namespace EPPlus.Fonts.OpenType.Integration.DataHolders -//{ -// //TextFragment is a portion of a longer string -// //the fragment itself may contain line-breaks -// internal class TextFragment -// { -// /// -// /// Char index starting position for this fragment -// /// -// internal int StartPos; - -// internal List IsPartOfLines; -// internal string ThisFragment; -// internal List PointWidthPerOutputLineInFragment = new(); -// internal List PointYIncreasePerLine = new(); - -// List positionsToBreakAt = new(); - -// internal float FontSize { get; set; } = float.NaN; - -// internal TextFragment(string thisFragment, int startPos) -// { -// ThisFragment = thisFragment; -// StartPos = startPos; -// } - -// internal int GetEndPosition() -// { -// return StartPos + ThisFragment.Length; -// } - -// /// -// /// Line-breaks to be added after wrapping -// /// -// /// -// internal void AddLocalLbPositon(int pos) -// { -// var locaLnPos = pos - StartPos; -// positionsToBreakAt.Add(locaLnPos); -// } - -// internal string GetLineBreakFreeFragment() -// { -// string freeFragment = ThisFragment.Replace("\r", ""); -// freeFragment.Replace("\n", ""); -// return freeFragment; -// } - -// internal string GetWrappedFragment() -// { -// if (positionsToBreakAt.Count == 0) -// { -// return ThisFragment; -// } - -// string alteredFragment = ThisFragment; -// int deleteSpaceCount = 0; -// for (int i = 0; i < positionsToBreakAt.Count; i++) -// { -// var insertPosition = positionsToBreakAt[i] + i - deleteSpaceCount; -// alteredFragment = alteredFragment.Insert(insertPosition, Environment.NewLine); -// //Spaces after inserted newlines are to be removed -// if (alteredFragment[insertPosition + Environment.NewLine.Length] == ' ') -// { -// alteredFragment = alteredFragment.Remove(insertPosition + Environment.NewLine.Length, 1); -// deleteSpaceCount++; -// } -// } - -// return alteredFragment; -// } -// } -//} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs new file mode 100644 index 000000000..2879bda9f --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration.DataHolders +{ + //TextFragment is a portion of a longer string + //the fragment itself may contain line-breaks + internal class TextFragmentAdvanced + { + /// + /// Char index starting position for this fragment + /// + internal int StartPos; + + internal List IsPartOfLines; + internal string ThisFragment; + internal List PointWidthPerOutputLineInFragment = new(); + internal List PointYIncreasePerLine = new(); + + List positionsToBreakAt = new(); + + internal float FontSize { get; set; } = float.NaN; + + internal TextFragmentAdvanced(string thisFragment, int startPos) + { + ThisFragment = thisFragment; + StartPos = startPos; + } + + internal int GetEndPosition() + { + return StartPos + ThisFragment.Length; + } + + /// + /// Line-breaks to be added after wrapping + /// + /// + internal void AddLocalLbPositon(int pos) + { + var locaLnPos = pos - StartPos; + positionsToBreakAt.Add(locaLnPos); + } + + internal string GetLineBreakFreeFragment() + { + string freeFragment = ThisFragment.Replace("\r", ""); + freeFragment.Replace("\n", ""); + return freeFragment; + } + + internal string GetWrappedFragment() + { + if (positionsToBreakAt.Count == 0) + { + return ThisFragment; + } + + string alteredFragment = ThisFragment; + int deleteSpaceCount = 0; + for (int i = 0; i < positionsToBreakAt.Count; i++) + { + var insertPosition = positionsToBreakAt[i] + i - deleteSpaceCount; + alteredFragment = alteredFragment.Insert(insertPosition, Environment.NewLine); + //Spaces after inserted newlines are to be removed + if (alteredFragment[insertPosition + Environment.NewLine.Length] == ' ') + { + alteredFragment = alteredFragment.Remove(insertPosition + Environment.NewLine.Length, 1); + deleteSpaceCount++; + } + } + + return alteredFragment; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs similarity index 100% rename from src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollection.cs rename to src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs index 1fe1a27af..ed9778360 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs @@ -16,5 +16,7 @@ public TextFragmentCollectionSimple(List fonts, List te Add(new TextFragment() { Font = fonts[i], Text = texts[i] }); } } + + } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLine.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLine.cs deleted file mode 100644 index dcc63f71e..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLine.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration -{ - public class TextLine - { - internal List richTextIndicies; - internal string content; - internal int startIndex; - internal List rtContentStartIndexPerRt; - internal int lastRtInternalIndex; - internal int startRtInternalIndex; - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs new file mode 100644 index 000000000..4735defbc --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -0,0 +1,99 @@ +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + + + +namespace EPPlus.Fonts.OpenType.Integration +{ + public class TextLineCollection : List, IEnumerable + { + + public List GetFragments() + { + List fragments = new List(); + + for (int i = 0; i< Lines.Count; i++) + { + var line = Lines[i]; + for (int j = 0; j< line.lineFragmentText.Count; j++) + { + var fragment = new TextFragment() + { + Text = line.lineFragmentText[j], + Font = GetFont(line.fragIds[j]), + }; + fragments.Add(fragment); + } + } + + return fragments; + } + + internal List Lines = new List(); + + List _originalFragments; + + internal MeasurementFont GetFont(int fragIdx) + { + return _originalFragments[fragIdx].Font; + } + + public TextLineCollection(List lines, List originalFragments) + { + _originalFragments = originalFragments; + + //foreach (var line in lines) + //{ + // foreach (var lf in line.LineFragments) + // { + // var text = line.GetLineFragmentText(lf); + // smallestTextFragments.Add(text); + // } + //} + + for(int i = 0; i < lines.Count; i++) + { + int lineNum = i; + Lines.Add(new TextLineVizualizer(this, lines[i], i)); + //foreach (var lf in _lines[i].LineFragments) + //{ + // var text = _lines[i].GetLineFragmentText(lf); + // var font = fonts[lf.RtFragIdx]; + // var details = _lines[i].LineFragments; + // //smallestTextFragments.Add(text); + //} + } + + //foreach (var line in _lines) + //{ + // //TextLineVizualizer visualizer = ne + // foreach (var lf in line.LineFragments) + // { + // var text = line.GetLineFragmentText(lf); + // var font = fonts[lf.RtFragIdx]; + // int lineNum + // //smallestTextFragments.Add(text); + // } + // //Lines.Add(new TextLineVizualizer(line)); + //} + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (int i = 0; i < Lines.Count; i++) + { + yield return Lines[i].TextLineDetails; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Lines.GetEnumerator(); + } + } +} + diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index e98fe3b46..350268bfe 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -1,11 +1,14 @@ using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.Integration.DataHolders; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; namespace EPPlus.Fonts.OpenType.Integration { + [DebuggerTypeProxy(typeof(TextLineSimpleVizualizer))] public class TextLineSimple { public List LineFragments { get; internal set; } = new List(); @@ -43,9 +46,9 @@ public double GetWidthWithoutTrailingSpaces() var trailingSpaceCount = 0; - for(int i = Text.Count()-1; i > 0; i--) + for (int i = Text.Count() - 1; i > 0; i--) { - if(Text[i] != ' ') + if (Text[i] != ' ') { break; } @@ -53,7 +56,7 @@ public double GetWidthWithoutTrailingSpaces() trailingSpaceCount++; } - if(WasWrappedOnSpace) + if (WasWrappedOnSpace) { trailingSpaceCount++; } @@ -68,7 +71,7 @@ public TextLineSimple() public TextLineSimple(string text, double largestFontSize, double largestAscent, double largestDescent) { - Text = text; + LineFragments = new LineFragmentCollection(text); LargestFontSize = largestFontSize; LargestAscent = largestAscent; LargestDescent = largestDescent; @@ -111,4 +114,47 @@ internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, d return newLineFragment; } } + + internal class TextLineSimpleVizualizer + { + public List Display + { + + get + { + List startIndices = new List(); + List lines = new List(); + + foreach(var fragment in _content.LineFragments ) + { + //startIndices.Add(fragment.StartIdx); + lines.Add(_content.GetLineFragmentText(fragment)+$" rtIdx:{fragment.RtFragIdx}"); + } + + //startIndices.Add(_content.Text.Length -1); + + //List lines = new List(); + + + //for (int i = 0; i < startIndices.Count -1; i++) + //{ + // var startidx = startIndices[i + 1]+1; + // var length = startidx - startIndices[i]; + // var substring = _content.Text.Substring(startIndices[i], length); + // lines.Add(substring); + //} + + + + return lines; + } + } + + private TextLineSimple _content; + + public TextLineSimpleVizualizer(TextLineSimple content) + { + _content = content; + } + } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs new file mode 100644 index 000000000..d59d14c86 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs @@ -0,0 +1,45 @@ +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration +{ + + public class TextLineVizualizer + { + TextLineCollection _parentCollection; + internal TextLineSimple TextLineDetails; + int _lineNum; + + internal List lineFragmentText = new List(); + internal List fragIds = new List(); + + //internal MeasurementFont font + //{ get + // { + // return _parentCollection.GetFont(TextLineDetails.) + // } + //} + + public TextLineVizualizer(TextLineCollection parentCollection, TextLineSimple line, int lineNum) + { + _lineNum = lineNum; + TextLineDetails = line; + _parentCollection = parentCollection; + + foreach(var lf in line.LineFragments) + { + lineFragmentText.Add(line.GetLineFragmentText(lf)); + fragIds.Add(lf.RtFragIdx); + } + //var text = _lines[i].GetLineFragmentText(lf); + //var font = fonts[lf.RtFragIdx]; + //var details = _lines[i].LineFragments; + + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index e563926c1..2c862a627 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -132,6 +132,8 @@ public List WrapRichTextLines( _lineListBuffer.Add(string.Empty); } + + return state.Lines; } From 45df22ccbec56ac38711293cf7df1224efa6ed89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 21 Apr 2026 16:01:14 +0200 Subject: [PATCH 02/26] Added complex dict for lookups --- .../TextFragmentCollectionAdvanced.cs | 803 +++++++++--------- .../DataHolders/TextLineCollection.cs | 46 +- .../DataHolders/TextLineVisualizer.cs | 14 +- 3 files changed, 444 insertions(+), 419 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs index cdd23b3bc..d849b8a74 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs @@ -1,388 +1,415 @@ -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; -//using System.Xml; - -//namespace EPPlus.Fonts.OpenType.Integration -//{ -// public class TextFragmentCollection -// { -// internal Dictionary CharLookup = new(); - -// internal List TextFragments = new(); - -// internal string AllText; -// internal List AllTextNewLineIndicies = new(); -// List _fragmentItems = new List(); -// List _lines = new List(); -// List outputFragments = new List(); -// //List outputStrings = new List(); -// internal List LargestFontSizePerLine { get; private set; } = new(); -// internal List AscentPerLine { get; private set; } = new(); -// internal List DescentPerLine { get; private set; } = new(); - -// /// -// /// Added linewidths in points -// /// -// internal List lineWidths { get; private set; } = new List(); - -// public List IndiciesToWrapAt {get; internal set; } - -// List TextLines = new(); - -// public TextFragmentCollection(List textFragments) -// { -// TextFragments = textFragments; -// IndiciesToWrapAt = new List(); - -// //Set data for each fragment and for AllText -// var currentTotalLength = 0; -// for (int i = 0; i < textFragments.Count; i++) -// { -// var currString = textFragments[i]; -// var fragment = new TextFragment(currString, currentTotalLength); -// currentTotalLength += currString.Length; -// _fragmentItems.Add(fragment); -// } - -// AllText = string.Join(string.Empty, textFragments.ToArray()); - -// //Get the indicies where newlines occur in the combined string -// AllTextNewLineIndicies = GetFirstCharPositionOfNewLines(AllText); - -// //Save minor information about each char so each char knows its line/fragment -// int charCount = 0; -// int lineIndex = 0; - -// List currFragments = new(); -// int lineStartCharIndex = 0; - -// //For each fragment -// for (int i = 0; i < TextFragments.Count; i++) -// { -// var textFragment = TextFragments[i]; - -// //For each char in current fragment -// for (int j = 0; j < textFragment.Length; j++) -// { -// if (lineIndex <= AllTextNewLineIndicies.Count - 1 && -// charCount >= AllTextNewLineIndicies[lineIndex]) -// { -// var text = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); -// var trimmedText = text.Trim(['\r', '\n']); - -// var line = new TextLine() -// { -// richTextIndicies = currFragments, -// content = trimmedText, -// startIndex = lineStartCharIndex, -// }; - -// lineStartCharIndex = AllTextNewLineIndicies[lineIndex]; -// _lines.Add(line); -// currFragments.Clear(); -// lineIndex++; -// } - -// var info = new CharInfo(charCount, i, lineIndex); -// CharLookup.Add(charCount, info); -// charCount++; -// } -// currFragments.Add(i); -// } - -// //Add the last line -// var lastText = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); -// var lastTrimmedText = lastText.Trim(['\r', '\n']); - -// var lastLine = new TextLine() -// { -// richTextIndicies = currFragments, -// content = lastTrimmedText, -// startIndex = lineStartCharIndex, -// }; - -// _lines.Add(lastLine); -// currFragments.Clear(); -// } - -// public TextFragmentCollection(List textFragments, List fontSizes) : this(textFragments) -// { -// if(textFragments.Count() != fontSizes.Count) -// { -// throw new InvalidOperationException($"TextFragment list and FontSizes list must be equal." + -// $"Counts:" + -// $"textFragment: {textFragments.Count()}" + -// $"fontSizes: {fontSizes.Count()}"); -// } - -// for(int i = 0; i< textFragments.Count; i++) -// { -// _fragmentItems[i].FontSize = fontSizes[i]; -// } -// } - -// private List GetFirstCharPositionOfNewLines(string stringsCombined) -// { -// List positions = new List(); -// for (int i = 0; i < stringsCombined.Length; i++) -// { -// if (stringsCombined[i] == '\n') -// { -// if (i < stringsCombined.Length - 1) -// { -// positions.Add(i + 1); -// } -// else -// { -// positions.Add(i); -// } -// } -// } -// return positions; -// } - -// internal void AddLargestFontSizePerLine(double fontSize) -// { -// LargestFontSizePerLine.Add(fontSize); -// } - -// internal void AddAscentPerLine(double Ascent) -// { -// AscentPerLine.Add(Ascent); -// } - -// internal void AddDescentPerLine(double Descent) -// { -// DescentPerLine.Add(Descent); -// } - -// internal void AddWrappingIndex(int index) -// { -// IndiciesToWrapAt.Add(index); -// } - -// public List GetFragmentsWithFinalLineBreaks() -// { -// //var newFragments = TextFragments.ToArray(); -// foreach(var i in IndiciesToWrapAt) -// { -// _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); -// } - -// foreach(var fragment in _fragmentItems) -// { -// outputFragments.Add(fragment.GetWrappedFragment()); -// } - -// return outputFragments; -// } - -// public List GetOutputLines() -// { -// var fragments = GetFragmentsWithFinalLineBreaks(); - -// string outputAllText = ""; - -// //Create final all text -// for (int i = 0; i < fragments.Count(); i++) -// { -// outputAllText += fragments[i]; -// } - -// var newLineIndiciesOutput = GetFirstCharPositionOfNewLines(outputAllText); -// List outputTextLines = new List(); - -// //Save minor information about each char so each char knows its line/fragment -// int charCount = 0; -// int lineIndex = 0; - -// List currFragments = new(); -// int lineStartCharIndex = 0; -// int RtInternalStartIndex = 0; - -// //For each fragment -// for (int i = 0; i < fragments.Count; i++) -// { -// var textFragment = fragments[i]; - -// //For each char in current fragment -// for (int j = 0; j < textFragment.Length; j++) -// { -// if (lineIndex <= newLineIndiciesOutput.Count - 1 && -// charCount >= newLineIndiciesOutput[lineIndex]) -// { -// //We have reached a new line -// var text = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); -// var trimmedText = text.Trim(['\r', '\n']); - -// var line = new TextLine() -// { -// richTextIndicies = currFragments, -// content = trimmedText, -// startIndex = lineStartCharIndex, -// rtContentStartIndexPerRt = new List(j), -// lastRtInternalIndex = j, -// startRtInternalIndex = RtInternalStartIndex -// }; - -// lineStartCharIndex = newLineIndiciesOutput[lineIndex]; -// outputTextLines.Add(line); -// currFragments.Clear(); -// RtInternalStartIndex = j; -// lineIndex++; -// } - -// //var info = new CharInfo(charCount, i, lineIndex); -// //CharLookup.Add(charCount, info); -// charCount++; -// } -// currFragments.Add(i); -// } - -// //Add the last line -// var lastText = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); -// var lastTrimmedText = lastText.Trim(['\r', '\n']); - -// var lastLine = new TextLine() -// { -// richTextIndicies = currFragments, -// content = lastTrimmedText, -// startIndex = lineStartCharIndex, -// lastRtInternalIndex = fragments.Last().Length, -// startRtInternalIndex = RtInternalStartIndex -// }; - -// outputTextLines.Add(lastLine); -// currFragments.Clear(); - -// return outputTextLines; -// } - -// //public void GetTextFragmentOutputStartIndiciesOnFinalLines(List finalOutputLines) -// //{ -// // var lineBreakStrings = GetFragmentsWithFinalLineBreaks(); -// // //var fragmentsWithoutLineBreaks = GetFragmentsWithoutLineBreaks(); -// // string outputAllText = ""; - -// // //List> eachFragmentForEachLine = new List>(); - -// // for(int i = 0; i< finalOutputLines.Count(); i++) -// // { -// // var line = new TextLine(); -// // line.content = finalOutputLines[i]; - -// // } - -// // //Create final all text -// // for(int i = 0; i< lineBreakStrings.Count(); i++) -// // { - -// // ////var length = lineBreakStrings[i].Length; -// // //outputAllText += lineBreakStrings[i]; -// // } - - -// //} - -// //public List GetSimpleTextLines(List wrappedLines) -// //{ -// // //The wrapped lines -// // var lines = wrappedLines; -// // //The richText data in a form that is one to one in chars -// // var frags = GetFragmentsWithoutLineBreaks(); - -// // int fragIdx = 0; -// // int charIdx = 0; -// // int lastFragLength = 0; - -// // var currentFragment = frags[fragIdx]; - -// // for (int i = 0; i< wrappedLines.Count(); i++) -// // { -// // var textLineSimple = new TextLineSimple(); - -// // for (int j = 0; j < wrappedLines[i].Length; j++) -// // { -// // textLineSimple -// // charIdx++; -// // } -// // } -// //} - -// public List GetFragmentsWithoutLineBreaks() -// { -// ////var newFragments = TextFragments.ToArray(); -// //foreach (var i in IndiciesToWrapAt) -// //{ -// // _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); -// //} - -// foreach (var fragment in _fragmentItems) -// { -// outputFragments.Add(fragment.GetLineBreakFreeFragment()); -// } - -// return outputFragments; -// } - -// internal void AddLineWidth(double width) -// { -// lineWidths.Add(width); -// } - -// /// -// /// Should arguably be done in constructor instead of created every time -// /// -// /// -// /// -// public List GetLargestFontSizesOfEachLine() -// { -// //List largestFontSizes = new List(); -// //foreach(var line in _lines) -// //{ -// // float largest = float.MinValue; -// // foreach(var idx in line.richTextIndicies) -// // { -// // if (float.IsNaN(_fragmentItems[idx].FontSize) == false && _fragmentItems[idx].FontSize > largest) -// // { -// // largest = _fragmentItems[idx].FontSize; -// // } -// // } -// // if (largest != float.MinValue) -// // { -// // largestFontSizes.Add(largest); -// // } -// //} -// //return largestFontSizes; -// return LargestFontSizePerLine; -// } - -// public double GetAscent(int lineIdx) -// { -// return AscentPerLine[lineIdx]; -// } - -// public double GetDescent(int lineIdx) -// { -// return DescentPerLine[lineIdx]; -// } - -// internal void AddFragmentWidth(int fragmentIdx, double width) -// { -// _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment.Add(width); -// } - -// public List GetFragmentWidths(int fragmentIdx) -// { -// return _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment; -// } - -// public List GetLinesFragmentIsPartOf(int fragmentIndex) -// { -// //Add throw if index does not exist? -// return _fragmentItems[fragmentIndex].IsPartOfLines; -// } -// } -//} +using EPPlus.Fonts.OpenType.Integration.DataHolders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; + +namespace EPPlus.Fonts.OpenType.Integration +{ + public class TextFragmentCollectionAdvanced + { + internal Dictionary CharLookup = new(); + + internal List TextFragments = new(); + + internal string AllText; + internal List AllTextNewLineIndicies = new(); + List _fragmentItems = new List(); + List _lines = new List(); + List outputFragments = new List(); + //List outputStrings = new List(); + internal List LargestFontSizePerLine { get; private set; } = new(); + internal List AscentPerLine { get; private set; } = new(); + internal List DescentPerLine { get; private set; } = new(); + + /// + /// Added linewidths in points + /// + internal List lineWidths { get; private set; } = new List(); + + public List IndiciesToWrapAt { get; internal set; } + + List TextLines = new(); + + public TextFragmentCollectionAdvanced(List textFragments) + { + TextFragments = textFragments; + IndiciesToWrapAt = new List(); + + //Set data for each fragment and for AllText + var currentTotalLength = 0; + for (int i = 0; i < textFragments.Count; i++) + { + var currString = textFragments[i]; + var fragment = new TextFragmentAdvanced(currString, currentTotalLength); + currentTotalLength += currString.Length; + _fragmentItems.Add(fragment); + } + + AllText = string.Join(string.Empty, textFragments.ToArray()); + + //Get the indicies where newlines occur in the combined string + AllTextNewLineIndicies = GetFirstCharPositionOfNewLines(AllText); + + //Save minor information about each char so each char knows its line/fragment + int charCount = 0; + int lineIndex = 0; + + List currFragments = new(); + int lineStartCharIndex = 0; + + //For each fragment + for (int i = 0; i < TextFragments.Count; i++) + { + var textFragment = TextFragments[i]; + + //For each char in current fragment + for (int j = 0; j < textFragment.Length; j++) + { + if (lineIndex <= AllTextNewLineIndicies.Count - 1 && + charCount >= AllTextNewLineIndicies[lineIndex]) + { + var text = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var trimmedText = text.Trim(['\r', '\n']); + + var line = new TextLineSimple(); + line.Text = trimmedText; + + foreach (var idx in currFragments) + { + var fragment = new LineFragment(i, idx); + } + + lineStartCharIndex = AllTextNewLineIndicies[lineIndex]; + _lines.Add(line); + currFragments.Clear(); + lineIndex++; + } + + var info = new CharInfo(charCount, i, lineIndex); + CharLookup.Add(charCount, info); + charCount++; + } + currFragments.Add(i); + } + + //Add the last line + var lastText = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var lastTrimmedText = lastText.Trim(['\r', '\n']); + + var lastLine = new TextLineSimple(); + lastLine.Text = lastTrimmedText; + + foreach (var idx in currFragments) + { + var fragment = new LineFragment(lineStartCharIndex, idx); + } + + _lines.Add(lastLine); + currFragments.Clear(); + } + + public TextFragmentCollectionAdvanced(List textFragments, List fontSizes) : this(textFragments) + { + if (textFragments.Count() != fontSizes.Count) + { + throw new InvalidOperationException($"TextFragment list and FontSizes list must be equal." + + $"Counts:" + + $"textFragment: {textFragments.Count()}" + + $"fontSizes: {fontSizes.Count()}"); + } + + for (int i = 0; i < textFragments.Count; i++) + { + _fragmentItems[i].FontSize = fontSizes[i]; + } + } + + private List GetFirstCharPositionOfNewLines(string stringsCombined) + { + List positions = new List(); + for (int i = 0; i < stringsCombined.Length; i++) + { + if (stringsCombined[i] == '\n') + { + if (i < stringsCombined.Length - 1) + { + positions.Add(i + 1); + } + else + { + positions.Add(i); + } + } + } + return positions; + } + + internal void AddLargestFontSizePerLine(double fontSize) + { + LargestFontSizePerLine.Add(fontSize); + } + + internal void AddAscentPerLine(double Ascent) + { + AscentPerLine.Add(Ascent); + } + + internal void AddDescentPerLine(double Descent) + { + DescentPerLine.Add(Descent); + } + + internal void AddWrappingIndex(int index) + { + IndiciesToWrapAt.Add(index); + } + + public List GetFragmentsWithFinalLineBreaks() + { + //var newFragments = TextFragments.ToArray(); + foreach (var i in IndiciesToWrapAt) + { + _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); + } + + foreach (var fragment in _fragmentItems) + { + outputFragments.Add(fragment.GetWrappedFragment()); + } + + return outputFragments; + } + + public List GetOutputLines() + { + var fragments = GetFragmentsWithFinalLineBreaks(); + + string outputAllText = ""; + + //Create final all text + for (int i = 0; i < fragments.Count(); i++) + { + outputAllText += fragments[i]; + } + + var newLineIndiciesOutput = GetFirstCharPositionOfNewLines(outputAllText); + List outputTextLines = new List(); + + //Save minor information about each char so each char knows its line/fragment + int charCount = 0; + int lineIndex = 0; + + List currFragments = new(); + int lineStartCharIndex = 0; + int RtInternalStartIndex = 0; + + //For each fragment + for (int i = 0; i < fragments.Count; i++) + { + var textFragment = fragments[i]; + + //For each char in current fragment + for (int j = 0; j < textFragment.Length; j++) + { + if (lineIndex <= newLineIndiciesOutput.Count - 1 && + charCount >= newLineIndiciesOutput[lineIndex]) + { + //We have reached a new line + var text = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var trimmedText = text.Trim(['\r', '\n']); + + var line = new TextLineSimple(); + line.Text = trimmedText; + + foreach (var idx in currFragments) + { + var fragment = new LineFragment(i, idx); + } + + + //var line = new TextLine() + //{ + // richTextIndicies = currFragments, + // content = trimmedText, + // startIndex = lineStartCharIndex, + // rtContentStartIndexPerRt = new List(j), + // lastRtInternalIndex = j, + // startRtInternalIndex = RtInternalStartIndex + //}; + + lineStartCharIndex = newLineIndiciesOutput[lineIndex]; + outputTextLines.Add(line); + currFragments.Clear(); + RtInternalStartIndex = j; + lineIndex++; + } + + //var info = new CharInfo(charCount, i, lineIndex); + //CharLookup.Add(charCount, info); + charCount++; + } + currFragments.Add(i); + } + + //Add the last line + var lastText = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var lastTrimmedText = lastText.Trim(['\r', '\n']); + + var lastLine = new TextLineSimple(); + lastLine.Text = lastTrimmedText; + + foreach (var idx in currFragments) + { + var fragment = new LineFragment(lineStartCharIndex, idx); + } + + _lines.Add(lastLine); + currFragments.Clear(); + + ////Add the last line + //var lastText = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + //var lastTrimmedText = lastText.Trim(['\r', '\n']); + + //var lastLine = new TextLine() + //{ + // richTextIndicies = currFragments, + // content = lastTrimmedText, + // startIndex = lineStartCharIndex, + // lastRtInternalIndex = fragments.Last().Length, + // startRtInternalIndex = RtInternalStartIndex + //}; + + //outputTextLines.Add(lastLine); + //currFragments.Clear(); + + return outputTextLines; + } + + //public void GetTextFragmentOutputStartIndiciesOnFinalLines(List finalOutputLines) + //{ + // var lineBreakStrings = GetFragmentsWithFinalLineBreaks(); + // //var fragmentsWithoutLineBreaks = GetFragmentsWithoutLineBreaks(); + // string outputAllText = ""; + + // //List> eachFragmentForEachLine = new List>(); + + // for(int i = 0; i< finalOutputLines.Count(); i++) + // { + // var line = new TextLine(); + // line.content = finalOutputLines[i]; + + // } + + // //Create final all text + // for(int i = 0; i< lineBreakStrings.Count(); i++) + // { + + // ////var length = lineBreakStrings[i].Length; + // //outputAllText += lineBreakStrings[i]; + // } + + + //} + + //public List GetSimpleTextLines(List wrappedLines) + //{ + // //The wrapped lines + // var lines = wrappedLines; + // //The richText data in a form that is one to one in chars + // var frags = GetFragmentsWithoutLineBreaks(); + + // int fragIdx = 0; + // int charIdx = 0; + // int lastFragLength = 0; + + // var currentFragment = frags[fragIdx]; + + // for (int i = 0; i< wrappedLines.Count(); i++) + // { + // var textLineSimple = new TextLineSimple(); + + // for (int j = 0; j < wrappedLines[i].Length; j++) + // { + // textLineSimple + // charIdx++; + // } + // } + //} + + public List GetFragmentsWithoutLineBreaks() + { + ////var newFragments = TextFragments.ToArray(); + //foreach (var i in IndiciesToWrapAt) + //{ + // _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); + //} + + foreach (var fragment in _fragmentItems) + { + outputFragments.Add(fragment.GetLineBreakFreeFragment()); + } + + return outputFragments; + } + + internal void AddLineWidth(double width) + { + lineWidths.Add(width); + } + + /// + /// Should arguably be done in constructor instead of created every time + /// + /// + /// + public List GetLargestFontSizesOfEachLine() + { + //List largestFontSizes = new List(); + //foreach(var line in _lines) + //{ + // float largest = float.MinValue; + // foreach(var idx in line.richTextIndicies) + // { + // if (float.IsNaN(_fragmentItems[idx].FontSize) == false && _fragmentItems[idx].FontSize > largest) + // { + // largest = _fragmentItems[idx].FontSize; + // } + // } + // if (largest != float.MinValue) + // { + // largestFontSizes.Add(largest); + // } + //} + //return largestFontSizes; + return LargestFontSizePerLine; + } + + public double GetAscent(int lineIdx) + { + return AscentPerLine[lineIdx]; + } + + public double GetDescent(int lineIdx) + { + return DescentPerLine[lineIdx]; + } + + internal void AddFragmentWidth(int fragmentIdx, double width) + { + _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment.Add(width); + } + + public List GetFragmentWidths(int fragmentIdx) + { + return _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment; + } + + public List GetLinesFragmentIsPartOf(int fragmentIndex) + { + //Add throw if index does not exist? + return _fragmentItems[fragmentIndex].IsPartOfLines; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index 4735defbc..f4d4db88c 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; @@ -9,6 +10,7 @@ namespace EPPlus.Fonts.OpenType.Integration { + [DebuggerDisplay("Lines = {Lines}")] public class TextLineCollection : List, IEnumerable { @@ -37,6 +39,15 @@ public List GetFragments() List _originalFragments; + /// + /// The id of the orginal fragment may correspond to + /// multiple lines with multiple different richtext fragments + /// So. fragIdLookup[fragId] returns dictionary of lines that contains the fragment + /// fragIdLookup[fragId][lineNum] returns list of output fragments that contain the font + /// fragIdLookup[fragId][lineNum][0] returns first richtextfragment within the line that contains the font. + /// + Dictionary>> fragIdLookup = new Dictionary>>(); + internal MeasurementFont GetFont(int fragIdx) { return _originalFragments[fragIdx].Font; @@ -46,40 +57,17 @@ public TextLineCollection(List lines, List origina { _originalFragments = originalFragments; - //foreach (var line in lines) - //{ - // foreach (var lf in line.LineFragments) - // { - // var text = line.GetLineFragmentText(lf); - // smallestTextFragments.Add(text); - // } - //} + for(int i = 0; i < originalFragments.Count; i++) + { + fragIdLookup.Add(i, new Dictionary>()); + } for(int i = 0; i < lines.Count; i++) { int lineNum = i; - Lines.Add(new TextLineVizualizer(this, lines[i], i)); - //foreach (var lf in _lines[i].LineFragments) - //{ - // var text = _lines[i].GetLineFragmentText(lf); - // var font = fonts[lf.RtFragIdx]; - // var details = _lines[i].LineFragments; - // //smallestTextFragments.Add(text); - //} - } - //foreach (var line in _lines) - //{ - // //TextLineVizualizer visualizer = ne - // foreach (var lf in line.LineFragments) - // { - // var text = line.GetLineFragmentText(lf); - // var font = fonts[lf.RtFragIdx]; - // int lineNum - // //smallestTextFragments.Add(text); - // } - // //Lines.Add(new TextLineVizualizer(line)); - //} + Lines.Add(new TextLineVizualizer(this, lines[i], i, ref fragIdLookup)); + } } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs index d59d14c86..c1d46f816 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs @@ -25,16 +25,26 @@ public class TextLineVizualizer // } //} - public TextLineVizualizer(TextLineCollection parentCollection, TextLineSimple line, int lineNum) + public TextLineVizualizer(TextLineCollection parentCollection, TextLineSimple line, int lineNum, + ref Dictionary>> fragmentLookup) { _lineNum = lineNum; TextLineDetails = line; _parentCollection = parentCollection; - + int fragCount = 0; foreach(var lf in line.LineFragments) { lineFragmentText.Add(line.GetLineFragmentText(lf)); fragIds.Add(lf.RtFragIdx); + + if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum)== false) + { + fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); + } + + fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); + + fragCount++; } //var text = _lines[i].GetLineFragmentText(lf); //var font = fonts[lf.RtFragIdx]; From 380da9baca8c144f7c6e3f0484b98cd276db3fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 11:00:35 +0200 Subject: [PATCH 03/26] Added substrings to linefrag. Clarified debug view --- .../RenderItems/Shared/ParagraphItem.cs | 2 +- .../DataHolders/TextLineSimpleTests.cs | 4 + .../TextFragmentCollectionAdvanced.cs | 2 +- .../DataHolders/TextLineCollection.cs | 52 ++++++++---- .../Integration/DataHolders/TextLineSimple.cs | 79 +++++++++++-------- .../DataHolders/TextLineVisualizer.cs | 37 ++++----- .../Integration/LineFragment.cs | 35 +++++++- .../Integration/TextLayoutEngine.RichText.cs | 14 ++-- .../Integration/WrapStateRichText.cs | 2 +- 9 files changed, 148 insertions(+), 79 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index be029a5db..c709e37a0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -468,7 +468,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth); + AddRenderItemTextRun(p.TextRuns[lineFragment.FragmentIndex], displayText, prevWidth); } TextRunItem runItem = Runs.Last(); diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index 205857ef2..f48500b79 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -24,6 +24,10 @@ public void TestLineFragmentAbstraction() var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + var collection = new TextLineCollection(wrappedLines, fragments); + + var frags = collection.ezFrags; + var line1 = wrappedLines[0]; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs index d849b8a74..16501d1a7 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs @@ -103,7 +103,7 @@ public TextFragmentCollectionAdvanced(List textFragments) foreach (var idx in currFragments) { - var fragment = new LineFragment(lineStartCharIndex, idx); + var fragment = new LineFragment(idx, lineStartCharIndex); } _lines.Add(lastLine); diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index f4d4db88c..849a4345f 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -10,9 +10,16 @@ namespace EPPlus.Fonts.OpenType.Integration { - [DebuggerDisplay("Lines = {Lines}")] + public struct ezFrag() + { + public string text; + public MeasurementFont font; + } + public class TextLineCollection : List, IEnumerable { + public ezFrag[] ezFrags; + public List individualFragments; public List GetFragments() { @@ -23,15 +30,26 @@ public List GetFragments() var line = Lines[i]; for (int j = 0; j< line.lineFragmentText.Count; j++) { - var fragment = new TextFragment() - { + var fragment = new TextFragment() + { Text = line.lineFragmentText[j], Font = GetFont(line.fragIds[j]), + AscentPoints = _originalFragments[j].AscentPoints, + DescentPoints = _originalFragments[j].DescentPoints }; fragments.Add(fragment); } } + ezFrags = new ezFrag[fragments.Count]; + + individualFragments = fragments; + + for (int i = 0; i < Lines.Count; i++) + { + ezFrags[i] = new ezFrag() { text = fragments[i].Text, font = fragments[i].Font }; + } + return fragments; } @@ -65,22 +83,24 @@ public TextLineCollection(List lines, List origina for(int i = 0; i < lines.Count; i++) { int lineNum = i; + int fragCount = 0; + foreach (var lf in lines[i].LineFragments) + { + //lineFragmentText.Add(line.GetLineFragmentText(lf)); + //fragIds.Add(lf.RtFragIdx); - Lines.Add(new TextLineVizualizer(this, lines[i], i, ref fragIdLookup)); - } - } + //if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum) == false) + //{ + // fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); + //} - IEnumerator IEnumerable.GetEnumerator() - { - for (int i = 0; i < Lines.Count; i++) - { - yield return Lines[i].TextLineDetails; - } - } + //fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); - IEnumerator IEnumerable.GetEnumerator() - { - return Lines.GetEnumerator(); + fragCount++; + } + Add(lines[i]); + } + GetFragments(); } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 350268bfe..1e7e3aea2 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -8,7 +8,8 @@ namespace EPPlus.Fonts.OpenType.Integration { - [DebuggerTypeProxy(typeof(TextLineSimpleVizualizer))] + //[DebuggerTypeProxy(typeof(TextLineSimpleVizualizer))] + [DebuggerDisplay("{Text}")] public class TextLineSimple { public List LineFragments { get; internal set; } = new List(); @@ -77,6 +78,18 @@ public TextLineSimple(string text, double largestFontSize, double largestAscent, LargestDescent = largestDescent; } + /// + /// Inserts the relevant substrings directly into the line fragments + /// + internal void CreateFinalizedSubstringsInLineFragments() + { + foreach (var lineFragment in LineFragments) + { + var text = GetLineFragmentText(lineFragment); + lineFragment.SetFinalizedText(text); + } + } + public string GetLineFragmentText(LineFragment rtFragment) { if (LineFragments.Contains(rtFragment) == false) @@ -106,7 +119,7 @@ public string GetLineFragmentText(LineFragment rtFragment) internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit) { //If we are splitting a fragment its position in the new line should be 0 - var newLineFragment = new LineFragment(origLf.RtFragIdx, 0); + var newLineFragment = new LineFragment(origLf.FragmentIndex, 0); newLineFragment.Width = origLf.Width - widthAtSplit; origLf.Width = widthAtSplit; @@ -115,46 +128,46 @@ internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, d } } - internal class TextLineSimpleVizualizer - { - public List Display - { + //internal class TextLineSimpleVizualizer + //{ + // public List Display + // { - get - { - List startIndices = new List(); - List lines = new List(); + // get + // { + // List startIndices = new List(); + // List lines = new List(); - foreach(var fragment in _content.LineFragments ) - { - //startIndices.Add(fragment.StartIdx); - lines.Add(_content.GetLineFragmentText(fragment)+$" rtIdx:{fragment.RtFragIdx}"); - } + // foreach(var fragment in _content.LineFragments ) + // { + // //startIndices.Add(fragment.StartIdx); + // lines.Add(_content.GetLineFragmentText(fragment)+$" rtIdx:{fragment.RtFragIdx}"); + // } - //startIndices.Add(_content.Text.Length -1); + // //startIndices.Add(_content.Text.Length -1); - //List lines = new List(); + // //List lines = new List(); - //for (int i = 0; i < startIndices.Count -1; i++) - //{ - // var startidx = startIndices[i + 1]+1; - // var length = startidx - startIndices[i]; - // var substring = _content.Text.Substring(startIndices[i], length); - // lines.Add(substring); - //} + // //for (int i = 0; i < startIndices.Count -1; i++) + // //{ + // // var startidx = startIndices[i + 1]+1; + // // var length = startidx - startIndices[i]; + // // var substring = _content.Text.Substring(startIndices[i], length); + // // lines.Add(substring); + // //} - return lines; - } - } + // return lines; + // } + // } - private TextLineSimple _content; + // private TextLineSimple _content; - public TextLineSimpleVizualizer(TextLineSimple content) - { - _content = content; - } - } + // public TextLineSimpleVizualizer(TextLineSimple content) + // { + // _content = content; + // } + //} } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs index c1d46f816..2b3a23494 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs @@ -12,7 +12,6 @@ namespace EPPlus.Fonts.OpenType.Integration public class TextLineVizualizer { TextLineCollection _parentCollection; - internal TextLineSimple TextLineDetails; int _lineNum; internal List lineFragmentText = new List(); @@ -25,27 +24,25 @@ public class TextLineVizualizer // } //} - public TextLineVizualizer(TextLineCollection parentCollection, TextLineSimple line, int lineNum, + public TextLineVizualizer(List fragments, int lineNum, ref Dictionary>> fragmentLookup) { - _lineNum = lineNum; - TextLineDetails = line; - _parentCollection = parentCollection; - int fragCount = 0; - foreach(var lf in line.LineFragments) - { - lineFragmentText.Add(line.GetLineFragmentText(lf)); - fragIds.Add(lf.RtFragIdx); - - if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum)== false) - { - fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); - } - - fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); - - fragCount++; - } + //_lineNum = lineNum; + //int fragCount = 0; + //foreach(var lf in fragments) + //{ + // lineFragmentText.Add(line.GetLineFragmentText(lf)); + // fragIds.Add(lf.RtFragIdx); + + // if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum)== false) + // { + // fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); + // } + + // fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); + + // fragCount++; + //} //var text = _lines[i].GetLineFragmentText(lf); //var font = fonts[lf.RtFragIdx]; //var details = _lines[i].LineFragments; diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 26dbc439b..1bc9e8c77 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; @@ -8,20 +9,50 @@ namespace EPPlus.Fonts.OpenType.Integration /// /// The fragment of a richTextFragment that is within a line /// + [DebuggerDisplay("{Text}")] public class LineFragment { + /// + /// Start char position within TextLineSimple.Text + /// public int StartIdx { get; set; } + /// + /// Width of this fragment + /// public double Width { get; set; } - public int RtFragIdx { get; set; } + /// + /// Index of the orginal TextFragment this line fragment came from + /// + public int FragmentIndex { get; set; } + /// + /// Width of a space in the original TextFragment + /// public double SpaceWidth { get; internal set; } + public string Text { get { return _finalFragmentText; } } + + //Seeing duplicated string in debugger creates clutter + //Better to check Text and then realise the private variable is the actual value holder + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string _finalFragmentText; + + /// + /// Only to be set after finishing all operations + /// Only to be set by TextLineSimple + /// This class should be essentially read only after setting this + /// + /// + internal void SetFinalizedText(string text) + { + _finalFragmentText = text; + } //public double AscentInPoints { get; private set; } //public double DescentInPoints { get; private set; } internal LineFragment(int rtFragmentIdx, int idxWithinLine/*, double ascentInPoints, double descentInPoints*/) { - RtFragIdx = rtFragmentIdx; + FragmentIndex = rtFragmentIdx; StartIdx = idxWithinLine; //AscentInPoints = ascentInPoints; //DescentInPoints = descentInPoints; diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 2c862a627..cbef97891 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -118,13 +118,15 @@ public List WrapRichTextLines( double largestDescent = 0; foreach (var lineFragment in line.LineFragments) { - var frag = fragments[lineFragment.RtFragIdx]; + var frag = fragments[lineFragment.FragmentIndex]; if (frag == null) continue; largestAscent = Math.Max(frag.AscentPoints, largestAscent); largestDescent = Math.Max(frag.DescentPoints, largestDescent); } line.LargestAscent = largestAscent; line.LargestDescent = largestDescent; + + line.CreateFinalizedSubstringsInLineFragments(); } if (_lineListBuffer.Count == 0) @@ -161,10 +163,12 @@ private void ProcessFragment( var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size); - state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); - state.LineFrag.SpaceWidth = spaceWidth; - state.LineFrag.StartIdx = lineBuilder.Length; - state.LineFrag.RtFragIdx = state.CurrentFragmentIdx; + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length) + { + SpaceWidth = spaceWidth, + StartIdx = lineBuilder.Length, + FragmentIndex = state.CurrentFragmentIdx + }; int i = 0; while (i < len) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index d645430ca..8d7f92af6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -65,7 +65,7 @@ internal void SetAndLogWordStartState(int wordStart) _listIdxWithinLine = CurrentTextLine.LineFragments.Count - 1; - if (CurrentTextLine.LineFragments[_listIdxWithinLine].RtFragIdx < _rtIdxAtWordStart) + if (CurrentTextLine.LineFragments[_listIdxWithinLine].FragmentIndex < _rtIdxAtWordStart) { //When the word begins we are on a fragment that has not yet been added to the list. //It will be the next index when added From 007a8f3cfeec9f07140d5ad63ca78c23556855b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 11:50:03 +0200 Subject: [PATCH 04/26] Successfully added func<> lookup in TextLinecollection --- .../DataHolders/TextLineCollection.cs | 24 ++++++++---- .../Integration/LineFragment.cs | 2 +- .../Integration/LineFragmentData.cs | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index 849a4345f..efdbde970 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -16,10 +16,12 @@ public struct ezFrag() public MeasurementFont font; } + [DebuggerDisplay("{this[2].LineFragments[1]} {LineFragments[3]}")] public class TextLineCollection : List, IEnumerable { public ezFrag[] ezFrags; public List individualFragments; + public List LineFragments = new List(); public List GetFragments() { @@ -84,17 +86,25 @@ public TextLineCollection(List lines, List origina { int lineNum = i; int fragCount = 0; + + lines[i].CreateFinalizedSubstringsInLineFragments(); + foreach (var lf in lines[i].LineFragments) { - //lineFragmentText.Add(line.GetLineFragmentText(lf)); - //fragIds.Add(lf.RtFragIdx); + var idx = lf.FragmentIndex; + + if (fragIdLookup[idx].ContainsKey(lineNum) == false) + { + fragIdLookup[idx].Add(lineNum, new List()); + } - //if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum) == false) - //{ - // fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); - //} + fragIdLookup[idx][lineNum].Add(fragCount); - //fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); + LineFragmentData data = new LineFragmentData( + () => { return _originalFragments[idx]; }, + () => { return lf.Width; }, + lines[i].GetLineFragmentText(lf)); + LineFragments.Add(data); fragCount++; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 1bc9e8c77..3c72b40c6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -21,7 +21,7 @@ public class LineFragment /// public double Width { get; set; } /// - /// Index of the orginal TextFragment this line fragment came from + /// Index of original TextFragment /// public int FragmentIndex { get; set; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs new file mode 100644 index 000000000..0d2a43ae2 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration +{ + [DebuggerDisplay("{Text}")] + public class LineFragmentData + { + + /// + /// Width of this fragment + /// + public double Width { get { return _getWidth(); } } + + /// + /// Text of this fragment + /// + public string Text { get; } + + /// + /// Original text fragment + /// + public TextFragment OriginalTextFragment { get { return _getTextFragment(); } } + + Func _getTextFragment; + Func _getWidth; + + internal LineFragmentData(Func getTextFragment, Func getWidth, string text) + { + _getTextFragment = getTextFragment; + _getWidth = getWidth; + Text = text; + } + } +} From 7c58862275222aba1fda35d8aee7d5de8e0812cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 15:16:04 +0200 Subject: [PATCH 05/26] Added richText interface+query collection fragment --- .../DataHolders/TextLineSimpleTests.cs | 25 +++- .../DataHolders/IRichTextInfoBase.cs | 38 +++++ .../TextFragmentCollectionSimple.cs | 2 - .../DataHolders/TextLineCollection.cs | 141 ++++++++++++++---- .../DataHolders/TextLineVisualizer.cs | 52 ------- .../Integration/LineFragmentData.cs | 3 + .../Integration/TextFragment.cs | 34 +++++ .../Integration/TextLayoutEngine.RichText.cs | 13 +- 8 files changed, 216 insertions(+), 92 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index f48500b79..39985cb8d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -4,6 +4,7 @@ using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -23,12 +24,30 @@ public void TestLineFragmentAbstraction() var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); - var collection = new TextLineCollection(wrappedLines, fragments); + var line1 = wrappedLines[0]; + } - var frags = collection.ezFrags; + [TestMethod] + public void TestLineFragmentSeeWhatLinesUseWhatRichText() + { - var line1 = wrappedLines[0]; + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + + var fragments = GetTextFragments(); + + fragments[4].RichTextOptions.FontColor = Color.DarkRed; + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); + var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); + + var lines = wrappedCollection.GetTextLinesThatUse(fragments[4]); + var specificFragments = wrappedCollection.GetLineFragmentsThatUse(fragments[4]); + + Assert.AreEqual(lines[0].LineFragments[1], specificFragments[0]); + Assert.AreEqual(fragments[4], wrappedCollection.LineFragments[6].OriginalTextFragment); + Assert.AreEqual(Color.DarkRed, wrappedCollection.LineFragments[6].OriginalTextFragment.RichTextOptions.FontColor); } List GetTextFragments() diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs new file mode 100644 index 000000000..12ad97970 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs @@ -0,0 +1,38 @@ +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + + +namespace EPPlus.Fonts.OpenType.Integration.DataHolders +{ + public interface IRichTextInfoBase + { + + bool IsItalic { get; set; } + bool IsBold { get; set; } + bool SubScript { get; set; } + bool SuperScript { get; set; } + + /// + /// Represents value in enum OfficeOpenXml.Style.eUnderlineType + /// + int UnderlineType { get; set; } + /// + /// Represents value in enum OfficeOpenXml.Style.eStrikeType + /// + int StrikeType { get; set; } + + #region potentially remove + /// + /// Represents OfficeOpenXml.Drawing.eTextCapsType + /// + int Capitalization { get; set; } + #endregion + + Color UnderlineColor { get; set; } + public Color FontColor { get; set; } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs index ed9778360..1fe1a27af 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionSimple.cs @@ -16,7 +16,5 @@ public TextFragmentCollectionSimple(List fonts, List te Add(new TextFragment() { Font = fonts[i], Text = texts[i] }); } } - - } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index efdbde970..fedc7595a 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -10,54 +10,89 @@ namespace EPPlus.Fonts.OpenType.Integration { - public struct ezFrag() - { - public string text; - public MeasurementFont font; - } - [DebuggerDisplay("{this[2].LineFragments[1]} {LineFragments[3]}")] public class TextLineCollection : List, IEnumerable { - public ezFrag[] ezFrags; - public List individualFragments; public List LineFragments = new List(); + List _originalFragments; - public List GetFragments() + /// + /// Returns null if fragment is not found in any lines + /// + /// + /// + /// + public List GetTextLinesThatUse(TextFragment fragment) { - List fragments = new List(); + var idx = _originalFragments.IndexOf(fragment); - for (int i = 0; i< Lines.Count; i++) + if(idx != -1) { - var line = Lines[i]; - for (int j = 0; j< line.lineFragmentText.Count; j++) + List retLines = new List(); + foreach (var key in fragIdLookup[idx].Keys) { - var fragment = new TextFragment() - { - Text = line.lineFragmentText[j], - Font = GetFont(line.fragIds[j]), - AscentPoints = _originalFragments[j].AscentPoints, - DescentPoints = _originalFragments[j].DescentPoints - }; - fragments.Add(fragment); + retLines.Add(this[key]); } + return retLines; } + else + { + return null; + } + } + /// + /// Returns null if fragment is not found in any linefragments + /// + /// + /// + public List GetLineFragmentsThatUse(TextFragment fragment) + { + var idx = _originalFragments.IndexOf(fragment); - ezFrags = new ezFrag[fragments.Count]; - - individualFragments = fragments; + List retFragments = null; - for (int i = 0; i < Lines.Count; i++) + if (idx != -1) { - ezFrags[i] = new ezFrag() { text = fragments[i].Text, font = fragments[i].Font }; + retFragments = new List(); + + foreach (var key in fragIdLookup[idx].Keys) + { + foreach(var lineFragment in fragIdLookup[idx][key]) + { + retFragments.Add(this[key].LineFragments[lineFragment]); + } + } } - return fragments; + return retFragments; } - internal List Lines = new List(); - - List _originalFragments; + ///// + ///// Returns null if fragment is not found in any linefragments + ///// + ///// + ///// + //public List GetLineFragmentDataThatUses(TextFragment fragment) + //{ + // var idx = _originalFragments.IndexOf(fragment); + + // List retFragments = null; + + // if (idx != -1) + // { + // retFragments = new List(); + + // foreach (var key in fragIdLookup[idx].Keys) + // { + // foreach (var lineFragment in fragIdLookup[idx][key]) + // { + // retFragments.Add(this[key].LineFragments[lineFragment]); + // } + // } + // } + + // return retFragments; + //} /// /// The id of the orginal fragment may correspond to @@ -73,6 +108,51 @@ internal MeasurementFont GetFont(int fragIdx) return _originalFragments[fragIdx].Font; } + public TextLineCollection(TextFragmentCollectionSimple fragmentCollection) + { + _originalFragments = fragmentCollection; + + for (int i = 0; i < fragmentCollection.Count; i++) + { + fragIdLookup.Add(i, new Dictionary>()); + } + } + + private void AddToDictionary(int idx, int lineNum, int fragPosInline) + { + + if (fragIdLookup[idx].ContainsKey(lineNum) == false) + { + fragIdLookup[idx].Add(lineNum, new List()); + } + + fragIdLookup[idx][lineNum].Add(fragPosInline); + } + + internal void FinalizeTextLineData(List lines) + { + for (int i = 0; i < lines.Count; i++) + { + int lineNum = i; + int fragCount = 0; + + foreach (var lf in lines[i].LineFragments) + { + var idx = lf.FragmentIndex; + AddToDictionary(idx, lineNum, fragCount); + + LineFragmentData data = new LineFragmentData( + () => { return _originalFragments[idx]; }, + () => { return lf.Width; }, + lines[i].GetLineFragmentText(lf)); + LineFragments.Add(data); + + fragCount++; + } + Add(lines[i]); + } + } + public TextLineCollection(List lines, List originalFragments) { _originalFragments = originalFragments; @@ -110,7 +190,6 @@ public TextLineCollection(List lines, List origina } Add(lines[i]); } - GetFragments(); } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs deleted file mode 100644 index 2b3a23494..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineVisualizer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration -{ - - public class TextLineVizualizer - { - TextLineCollection _parentCollection; - int _lineNum; - - internal List lineFragmentText = new List(); - internal List fragIds = new List(); - - //internal MeasurementFont font - //{ get - // { - // return _parentCollection.GetFont(TextLineDetails.) - // } - //} - - public TextLineVizualizer(List fragments, int lineNum, - ref Dictionary>> fragmentLookup) - { - //_lineNum = lineNum; - //int fragCount = 0; - //foreach(var lf in fragments) - //{ - // lineFragmentText.Add(line.GetLineFragmentText(lf)); - // fragIds.Add(lf.RtFragIdx); - - // if (fragmentLookup[lf.RtFragIdx].ContainsKey(lineNum)== false) - // { - // fragmentLookup[lf.RtFragIdx].Add(lineNum, new List()); - // } - - // fragmentLookup[lf.RtFragIdx][lineNum].Add(fragCount); - - // fragCount++; - //} - //var text = _lines[i].GetLineFragmentText(lf); - //var font = fonts[lf.RtFragIdx]; - //var details = _lines[i].LineFragments; - - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs index 0d2a43ae2..2822d8243 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs @@ -25,8 +25,11 @@ public class LineFragmentData /// public TextFragment OriginalTextFragment { get { return _getTextFragment(); } } + + #region Callbacks Func _getTextFragment; Func _getWidth; + #endregion internal LineFragmentData(Func getTextFragment, Func getWidth, string text) { diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs index bf7bb0e05..4f55773ba 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs @@ -10,8 +10,10 @@ Date Author Change ************************************************************************************************* 01/20/2025 EPPlus Software AB TextLayoutEngine implementation *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Integration.DataHolders; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.Fonts; +using System.Drawing; namespace EPPlus.Fonts.OpenType.Integration { @@ -21,10 +23,42 @@ namespace EPPlus.Fonts.OpenType.Integration public class TextFragment { public string Text { get; set; } + public MeasurementFont Font { get; set; } public ShapingOptions Options { get; set; } + /// + /// Store rich-text info. + /// Nothing is supposed to be done with this within OpenType + /// but we hold the data so users may more easily recognize what rich text this is in the output. + /// + public IRichTextInfoBase RichTextOptions { get; set; } = new RichTextDefaults(); + public double AscentPoints { get; set; } public double DescentPoints { get; set; } } + + public class RichTextDefaults : IRichTextInfoBase + { + internal RichTextDefaults() + { + } + public bool IsItalic { get; set; } = false; + + public bool IsBold { get; set; } = false; + + public bool SubScript { get; set; } = false; + + public bool SuperScript { get; set; } = false; + + public int UnderlineType { get; set; } = -1; + + public int StrikeType { get; set; } = -1; + + public int Capitalization { get; set; } = -1; + + public Color UnderlineColor { get; set; } + + public Color FontColor { get; set; } + } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index cbef97891..d8285a7cc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -82,6 +82,15 @@ public List WrapRichTextLines( return WrapRichTextLines(tCollection, maxWidthPoints); } + public TextLineCollection WrapRichTextLineCollection( + List fragments, + double maxWidthPoints) + { + var innerLines = WrapRichTextLines(fragments, maxWidthPoints); + var collection = new TextLineCollection(innerLines, fragments); + return collection; + } + public List WrapRichTextLines( List fragments, double maxWidthPoints) @@ -125,8 +134,6 @@ public List WrapRichTextLines( } line.LargestAscent = largestAscent; line.LargestDescent = largestDescent; - - line.CreateFinalizedSubstringsInLineFragments(); } if (_lineListBuffer.Count == 0) @@ -134,8 +141,6 @@ public List WrapRichTextLines( _lineListBuffer.Add(string.Empty); } - - return state.Lines; } From cb4d6ec696c419cd57ba86f3fc54ad234fb7cdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 15:17:35 +0200 Subject: [PATCH 06/26] Renamed LineFragments and made internal --- .../DataHolders/TextLineSimpleTests.cs | 2 +- .../Integration/TextLayoutEngineTests.cs | 24 +++++++------- .../TextFragmentCollectionTests.cs | 32 +++++++++---------- .../DataHolders/TextLineCollection.cs | 6 ++-- .../Integration/DataHolders/TextLineSimple.cs | 16 +++++----- .../Integration/TextLayoutEngine.RichText.cs | 4 +-- .../Integration/WrapStateRichText.cs | 24 +++++++------- 7 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index 39985cb8d..6707d96d7 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -45,7 +45,7 @@ public void TestLineFragmentSeeWhatLinesUseWhatRichText() var lines = wrappedCollection.GetTextLinesThatUse(fragments[4]); var specificFragments = wrappedCollection.GetLineFragmentsThatUse(fragments[4]); - Assert.AreEqual(lines[0].LineFragments[1], specificFragments[0]); + Assert.AreEqual(lines[0].InternalLineFragments[1], specificFragments[0]); Assert.AreEqual(fragments[4], wrappedCollection.LineFragments[6].OriginalTextFragment); Assert.AreEqual(Color.DarkRed, wrappedCollection.LineFragments[6].OriginalTextFragment.RichTextOptions.FontColor); } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 7f91bcd57..c560fe1f1 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -524,8 +524,8 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); Assert.AreEqual(pointsTotal, wrappedLines[0].Width); - Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width); - Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width); + Assert.AreEqual(points1, wrappedLines[0].InternalLineFragments[0].Width); + Assert.AreEqual(points2, wrappedLines[0].InternalLineFragments[1].Width); } [TestMethod] @@ -574,8 +574,8 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); Assert.AreEqual(pointsTotal, wrappedLines[0].Width); - Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width); - Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width); + Assert.AreEqual(points1, wrappedLines[0].InternalLineFragments[0].Width); + Assert.AreEqual(points2, wrappedLines[0].InternalLineFragments[1].Width); var noSpaceWidth = wrappedLines[0].GetWidthWithoutTrailingSpaces(); Assert.AreEqual(202.8916f, noSpaceWidth); } @@ -631,7 +631,7 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); - Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); + Assert.AreEqual(12.55224609375d, wrappedLines[0].InternalLineFragments[2].Width); Assert.AreEqual(202.8916f, wrappedLines[1].GetWidthWithoutTrailingSpaces()); List smallestTextFragments = new List(); @@ -639,7 +639,7 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() //Ensure each linefragment can get correct text foreach(var line in wrappedLines) { - foreach(var lf in line.LineFragments) + foreach(var lf in line.InternalLineFragments) { var text = line.GetLineFragmentText(lf); smallestTextFragments.Add(text); @@ -736,25 +736,25 @@ public void WrapRichTextDifficultCaseCompare() Assert.AreEqual(210.890625d, wrappedLines[3].Width, epsilon); Assert.AreEqual(127.04296875d, wrappedLines[4].Width, epsilon); - var line1FragmentsNew = wrappedLines[0].LineFragments; + var line1FragmentsNew = wrappedLines[0].InternalLineFragments; Assert.AreEqual(32.87646484375d, line1FragmentsNew[0].Width, epsilon); - var line2FragmentsNew = wrappedLines[1].LineFragments; + var line2FragmentsNew = wrappedLines[1].InternalLineFragments; Assert.AreEqual(5.30126953125d, line2FragmentsNew[0].Width, epsilon); - var line3FragmentsNew = wrappedLines[2].LineFragments; + var line3FragmentsNew = wrappedLines[2].InternalLineFragments; Assert.AreEqual(40.21875d, line3FragmentsNew[0].Width, epsilon); Assert.AreEqual(52.16943359375d, line3FragmentsNew[1].Width, epsilon); Assert.AreEqual(12.55712890625d, line3FragmentsNew[2].Width, epsilon); - var line4FragmentsNew = wrappedLines[3].LineFragments; + var line4FragmentsNew = wrappedLines[3].InternalLineFragments; Assert.AreEqual(24.86328125d, line4FragmentsNew[0].Width, epsilon); Assert.AreEqual(186.02734375d, line4FragmentsNew[1].Width, epsilon); - var line5FragmentsNew = wrappedLines[4].LineFragments; + var line5FragmentsNew = wrappedLines[4].InternalLineFragments; Assert.AreEqual(26.390625d, line5FragmentsNew[0].Width, epsilon); Assert.AreEqual(100.65234375d, line5FragmentsNew[1].Width, epsilon); @@ -983,7 +983,7 @@ public void VerifyWrappingSingleChar() var wrappedLines = layout.WrapRichTextLines(fragments, maxWidthPt); - Assert.AreEqual(0, wrappedLines[1].LineFragments[0].StartIdx); + Assert.AreEqual(0, wrappedLines[1].InternalLineFragments[0].StartIdx); } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs index 6710875f3..e5fab0817 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs @@ -124,7 +124,7 @@ public void MeasureWrappedWidthsWithInternalLineBreaks() var line1 = wrappedLines[0]; - var pixels11 = Math.Round(line1.LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels11 = Math.Round(line1.InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeline1 = Math.Round(line1.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); Assert.AreEqual(44, pixels11); @@ -132,7 +132,7 @@ public void MeasureWrappedWidthsWithInternalLineBreaks() var line2 = wrappedLines[1]; - var pixels21 = Math.Round(line2.LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels21 = Math.Round(line2.InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeline2 = Math.Round(line2.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); Assert.AreEqual(7, pixels21); @@ -140,9 +140,9 @@ public void MeasureWrappedWidthsWithInternalLineBreaks() var line3 = wrappedLines[2]; - var pixels31 = Math.Round(line3.LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels32 = Math.Round(line3.LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels33 = Math.Round(line3.LineFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels31 = Math.Round(line3.InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels32 = Math.Round(line3.InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels33 = Math.Round(line3.InternalLineFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine3 = Math.Round(line3.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); //~54 px @@ -157,8 +157,8 @@ public void MeasureWrappedWidthsWithInternalLineBreaks() var line4 = wrappedLines[3]; - var pixels41 = Math.Round(line4.LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels42 = Math.Round(line4.LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels41 = Math.Round(line4.InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels42 = Math.Round(line4.InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine4 = Math.Round(line4.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); //~34 px Assert.AreEqual(33, pixels41); @@ -169,8 +169,8 @@ public void MeasureWrappedWidthsWithInternalLineBreaks() var line5 = wrappedLines[4]; - var pixels51 = Math.Round(line5.LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels52 = Math.Round(line5.LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels51 = Math.Round(line5.InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels52 = Math.Round(line5.InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine5 = Math.Round(line5.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); Assert.AreEqual(35, pixels51); //This line does NOT contain a space at the end @@ -230,9 +230,9 @@ public void MeasureWrappedWidths() var wrappedLines = ttMeasurer.WrapRichTextLines(textFragments, maxSizePoints); - var pixels1 = Math.Round(wrappedLines[0].LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels2 = Math.Round(wrappedLines[0].LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels3 = Math.Round(wrappedLines[0].LineFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels1 = Math.Round(wrappedLines[0].InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels2 = Math.Round(wrappedLines[0].InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels3 = Math.Round(wrappedLines[0].InternalLineFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine = Math.Round(wrappedLines[0].Width.PointToPixel(),0, MidpointRounding.AwayFromZero); //~54 px @@ -245,8 +245,8 @@ public void MeasureWrappedWidths() //Total Width: ~140 Assert.AreEqual(140d, pixelsWholeLine); - var pixels21 = Math.Round(wrappedLines[1].LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels22 = Math.Round(wrappedLines[1].LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels21 = Math.Round(wrappedLines[1].InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels22 = Math.Round(wrappedLines[1].InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine2 = Math.Round(wrappedLines[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); //~34 px Assert.AreEqual(33, pixels21); @@ -255,8 +255,8 @@ public void MeasureWrappedWidths() Assert.AreEqual(281, pixelsWholeLine2); - var pixels31 = Math.Round(wrappedLines[2].LineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); - var pixels32 = Math.Round(wrappedLines[2].LineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels31 = Math.Round(wrappedLines[2].InternalLineFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels32 = Math.Round(wrappedLines[2].InternalLineFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); var pixelsWholeLine3 = Math.Round(wrappedLines[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); Assert.AreEqual(35, pixels31); //This line does NOT contain a space at the end diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index fedc7595a..681e95aaa 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -59,7 +59,7 @@ public List GetLineFragmentsThatUse(TextFragment fragment) { foreach(var lineFragment in fragIdLookup[idx][key]) { - retFragments.Add(this[key].LineFragments[lineFragment]); + retFragments.Add(this[key].InternalLineFragments[lineFragment]); } } } @@ -136,7 +136,7 @@ internal void FinalizeTextLineData(List lines) int lineNum = i; int fragCount = 0; - foreach (var lf in lines[i].LineFragments) + foreach (var lf in lines[i].InternalLineFragments) { var idx = lf.FragmentIndex; AddToDictionary(idx, lineNum, fragCount); @@ -169,7 +169,7 @@ public TextLineCollection(List lines, List origina lines[i].CreateFinalizedSubstringsInLineFragments(); - foreach (var lf in lines[i].LineFragments) + foreach (var lf in lines[i].InternalLineFragments) { var idx = lf.FragmentIndex; diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 1e7e3aea2..cd9a0e976 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -12,7 +12,7 @@ namespace EPPlus.Fonts.OpenType.Integration [DebuggerDisplay("{Text}")] public class TextLineSimple { - public List LineFragments { get; internal set; } = new List(); + internal List InternalLineFragments { get; set; } = new List(); public string Text { get; internal set; } /// @@ -43,7 +43,7 @@ public class TextLineSimple /// public double GetWidthWithoutTrailingSpaces() { - lastFontSpaceWidth = LineFragments.Last().SpaceWidth; + lastFontSpaceWidth = InternalLineFragments.Last().SpaceWidth; var trailingSpaceCount = 0; @@ -72,7 +72,7 @@ public TextLineSimple() public TextLineSimple(string text, double largestFontSize, double largestAscent, double largestDescent) { - LineFragments = new LineFragmentCollection(text); + InternalLineFragments = new LineFragmentCollection(text); LargestFontSize = largestFontSize; LargestAscent = largestAscent; LargestDescent = largestDescent; @@ -83,7 +83,7 @@ public TextLineSimple(string text, double largestFontSize, double largestAscent, /// internal void CreateFinalizedSubstringsInLineFragments() { - foreach (var lineFragment in LineFragments) + foreach (var lineFragment in InternalLineFragments) { var text = GetLineFragmentText(lineFragment); lineFragment.SetFinalizedText(text); @@ -92,7 +92,7 @@ internal void CreateFinalizedSubstringsInLineFragments() public string GetLineFragmentText(LineFragment rtFragment) { - if (LineFragments.Contains(rtFragment) == false) + if (InternalLineFragments.Contains(rtFragment) == false) { throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); } @@ -104,14 +104,14 @@ public string GetLineFragmentText(LineFragment rtFragment) var startIdx = rtFragment.StartIdx; - var idxInLst = LineFragments.FindIndex(x => x == rtFragment); - if (idxInLst == LineFragments.Count - 1) + var idxInLst = InternalLineFragments.FindIndex(x => x == rtFragment); + if (idxInLst == InternalLineFragments.Count - 1) { return Text.Substring(startIdx, Text.Length - startIdx); } else { - var endIdx = LineFragments[idxInLst + 1].StartIdx; + var endIdx = InternalLineFragments[idxInLst + 1].StartIdx; return Text.Substring(startIdx, endIdx - startIdx); } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index d8285a7cc..dcb2f41ec 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -125,7 +125,7 @@ public List WrapRichTextLines( { double largestAscent = 0; double largestDescent = 0; - foreach (var lineFragment in line.LineFragments) + foreach (var lineFragment in line.InternalLineFragments) { var frag = fragments[lineFragment.FragmentIndex]; if (frag == null) continue; @@ -213,7 +213,7 @@ private void ProcessFragment( if (state.LineFrag.Width > 0) { - state.CurrentTextLine.LineFragments.Add(state.LineFrag); + state.CurrentTextLine.InternalLineFragments.Add(state.LineFrag); } state.CurrentFragmentIdx++; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 8d7f92af6..907b5ddad 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -15,9 +15,9 @@ public WrapStateRichText(double lineWidth) internal void EndCurrentTextLine() { - if (CurrentTextLine.LineFragments.Contains(LineFrag) == false) + if (CurrentTextLine.InternalLineFragments.Contains(LineFrag) == false) { - CurrentTextLine.LineFragments.Add(LineFrag); + CurrentTextLine.InternalLineFragments.Add(LineFrag); } Lines.Add(CurrentTextLine); } @@ -36,7 +36,7 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) else { //_fragmentsForNextLine.Add(LineFrag); - nextLine.LineFragments = _fragmentsForNextLine; + nextLine.InternalLineFragments = _fragmentsForNextLine; //LineFrag.StartIdx = ; Lines.Add(CurrentTextLine); @@ -57,15 +57,15 @@ internal void SetAndLogWordStartState(int wordStart) _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; - if(CurrentTextLine.LineFragments.Count == 0) + if(CurrentTextLine.InternalLineFragments.Count == 0) { _listIdxWithinLine = 0; return; } - _listIdxWithinLine = CurrentTextLine.LineFragments.Count - 1; + _listIdxWithinLine = CurrentTextLine.InternalLineFragments.Count - 1; - if (CurrentTextLine.LineFragments[_listIdxWithinLine].FragmentIndex < _rtIdxAtWordStart) + if (CurrentTextLine.InternalLineFragments[_listIdxWithinLine].FragmentIndex < _rtIdxAtWordStart) { //When the word begins we are on a fragment that has not yet been added to the list. //It will be the next index when added @@ -91,23 +91,23 @@ internal void AdjustLineFragmentsForNextLine() { //If we are On the fragment we have not added it yet //Do so before splitting - CurrentTextLine.LineFragments.Add(LineFrag); + CurrentTextLine.InternalLineFragments.Add(LineFrag); } - var origFragment = CurrentTextLine.LineFragments[_listIdxWithinLine]; + var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart); - CurrentTextLine.LineFragments[_listIdxWithinLine] = origFragment; + CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; _fragmentsForNextLine = new List(); //Iterate backwards from back of list until we hit fragment - for (int i = CurrentTextLine.LineFragments.Count()-1; i > _listIdxWithinLine; i--) + for (int i = CurrentTextLine.InternalLineFragments.Count()-1; i > _listIdxWithinLine; i--) { //Add fragment to the new list - _fragmentsForNextLine.Insert(0, CurrentTextLine.LineFragments[i]); + _fragmentsForNextLine.Insert(0, CurrentTextLine.InternalLineFragments[i]); //Remove it from the old - CurrentTextLine.LineFragments.RemoveAt(i); + CurrentTextLine.InternalLineFragments.RemoveAt(i); } if (_rtIdxAtWordStart != CurrentFragmentIdx) From 2d4afa5e259eba1a9f3ee6888831383667ab5a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 15:41:20 +0200 Subject: [PATCH 07/26] Finalized lineFragmentData as LineFragments --- .../RenderItems/Shared/ParagraphItem.cs | 7 ++-- .../DataHolders/TextLineSimpleTests.cs | 2 ++ .../DataHolders/TextLineCollection.cs | 8 +++-- .../Integration/DataHolders/TextLineSimple.cs | 35 +++++++++++++++---- .../Integration/LineFragment.cs | 31 ++++++++-------- .../Integration/LineFragmentData.cs | 8 +++-- .../Integration/TextLayoutEngine.RichText.cs | 2 ++ 7 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index c709e37a0..b5f32f34c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -345,7 +345,7 @@ internal void AddLinesAndTextRuns(string textIfEmpty) foreach (var lineFragment in line.LineFragments) { - var displayText = line.GetLineFragmentText(lineFragment); + var displayText = lineFragment.Text; if (string.IsNullOrEmpty(textIfEmpty) == false) { @@ -460,7 +460,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) foreach (var lineFragment in line.LineFragments) { - var displayText = line.GetLineFragmentText(lineFragment); + var displayText = lineFragment.Text; if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) { @@ -468,7 +468,8 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - AddRenderItemTextRun(p.TextRuns[lineFragment.FragmentIndex], displayText, prevWidth); + var idx = _newTextFragments.IndexOf(lineFragment.OriginalTextFragment); + AddRenderItemTextRun(p.TextRuns[idx], displayText, prevWidth); } TextRunItem runItem = Runs.Last(); diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index 6707d96d7..5d662147a 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -40,6 +40,8 @@ public void TestLineFragmentSeeWhatLinesUseWhatRichText() fragments[4].RichTextOptions.FontColor = Color.DarkRed; var layout = OpenTypeFonts.GetTextLayoutEngineForFont(fragments[0].Font); + + var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); var lines = wrappedCollection.GetTextLinesThatUse(fragments[4]); diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index 681e95aaa..9551f6d53 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -144,7 +144,8 @@ internal void FinalizeTextLineData(List lines) LineFragmentData data = new LineFragmentData( () => { return _originalFragments[idx]; }, () => { return lf.Width; }, - lines[i].GetLineFragmentText(lf)); + lines[i].GetLineFragmentText(lf), + lf.StartIdx); LineFragments.Add(data); fragCount++; @@ -167,7 +168,7 @@ public TextLineCollection(List lines, List origina int lineNum = i; int fragCount = 0; - lines[i].CreateFinalizedSubstringsInLineFragments(); + //lines[i].CreateFinalizedSubstringsInLineFragments(); foreach (var lf in lines[i].InternalLineFragments) { @@ -183,7 +184,8 @@ public TextLineCollection(List lines, List origina LineFragmentData data = new LineFragmentData( () => { return _originalFragments[idx]; }, () => { return lf.Width; }, - lines[i].GetLineFragmentText(lf)); + lines[i].GetLineFragmentText(lf), + lf.StartIdx); LineFragments.Add(data); fragCount++; diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index cd9a0e976..a5e884f5a 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -12,8 +12,16 @@ namespace EPPlus.Fonts.OpenType.Integration [DebuggerDisplay("{Text}")] public class TextLineSimple { + /// + /// Internal data for making operations + /// internal List InternalLineFragments { get; set; } = new List(); + /// + /// External output data for reading + /// + public List LineFragments { get; internal set; } = new List(); + public string Text { get; internal set; } /// /// The largest font size within this line @@ -78,15 +86,28 @@ public TextLineSimple(string text, double largestFontSize, double largestAscent, LargestDescent = largestDescent; } - /// - /// Inserts the relevant substrings directly into the line fragments - /// - internal void CreateFinalizedSubstringsInLineFragments() + ///// + ///// Inserts the relevant substrings directly into the line fragments + ///// + //internal void CreateFinalizedSubstringsInLineFragments() + //{ + // foreach (var lineFragment in InternalLineFragments) + // { + // var text = GetLineFragmentText(lineFragment); + // lineFragment.SetFinalizedText(text); + // } + //} + + internal void FinalizeLineFragments(List originalFragments) { - foreach (var lineFragment in InternalLineFragments) + foreach (var lf in InternalLineFragments) { - var text = GetLineFragmentText(lineFragment); - lineFragment.SetFinalizedText(text); + LineFragmentData data = new LineFragmentData( + () => { return originalFragments[lf.FragmentIndex]; }, + () => { return lf.Width; }, + GetLineFragmentText(lf), + lf.StartIdx); + LineFragments.Add(data); } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 3c72b40c6..ee7f18e62 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -9,7 +9,6 @@ namespace EPPlus.Fonts.OpenType.Integration /// /// The fragment of a richTextFragment that is within a line /// - [DebuggerDisplay("{Text}")] public class LineFragment { /// @@ -30,23 +29,23 @@ public class LineFragment /// public double SpaceWidth { get; internal set; } - public string Text { get { return _finalFragmentText; } } + //public string Text { get { return _finalFragmentText; } } - //Seeing duplicated string in debugger creates clutter - //Better to check Text and then realise the private variable is the actual value holder - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string _finalFragmentText; + ////Seeing duplicated string in debugger creates clutter + ////Better to check Text and then realise the private variable is the actual value holder + //[DebuggerBrowsable(DebuggerBrowsableState.Never)] + //private string _finalFragmentText; - /// - /// Only to be set after finishing all operations - /// Only to be set by TextLineSimple - /// This class should be essentially read only after setting this - /// - /// - internal void SetFinalizedText(string text) - { - _finalFragmentText = text; - } + ///// + ///// Only to be set after finishing all operations + ///// Only to be set by TextLineSimple + ///// This class should be essentially read only after setting this + ///// + ///// + //internal void SetFinalizedText(string text) + //{ + // _finalFragmentText = text; + //} //public double AscentInPoints { get; private set; } //public double DescentInPoints { get; private set; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs index 2822d8243..b241d4dfd 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs @@ -9,7 +9,10 @@ namespace EPPlus.Fonts.OpenType.Integration [DebuggerDisplay("{Text}")] public class LineFragmentData { - + /// + /// Char idx within the line + /// + public int StartIdx { get; } /// /// Width of this fragment /// @@ -31,11 +34,12 @@ public class LineFragmentData Func _getWidth; #endregion - internal LineFragmentData(Func getTextFragment, Func getWidth, string text) + internal LineFragmentData(Func getTextFragment, Func getWidth, string text, int startidx) { _getTextFragment = getTextFragment; _getWidth = getWidth; Text = text; + StartIdx = startidx; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index dcb2f41ec..d8d6eaf66 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -134,6 +134,8 @@ public List WrapRichTextLines( } line.LargestAscent = largestAscent; line.LargestDescent = largestDescent; + + line.FinalizeLineFragments(fragments); } if (_lineListBuffer.Count == 0) From ecb675a4caded3136e16a15deb387b71e923748e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 16:36:09 +0200 Subject: [PATCH 08/26] Added linenum method + cleanup --- .../DataHolders/TextLineSimpleTests.cs | 10 +- .../DataHolders/TextLineCollection.cs | 131 +++++++++--------- .../Integration/DataHolders/TextLineSimple.cs | 9 +- .../Integration/LineFragment.cs | 24 +--- ...eFragmentData.cs => LineFragmentOutput.cs} | 16 ++- 5 files changed, 93 insertions(+), 97 deletions(-) rename src/EPPlus.Fonts.OpenType/Integration/{LineFragmentData.cs => LineFragmentOutput.cs} (55%) diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index 5d662147a..ca683ee1c 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -17,7 +17,6 @@ public class TextLineSimpleTests [TestMethod] public void TestLineFragmentAbstraction() { - var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); var fragments = GetTextFragments(); @@ -26,13 +25,12 @@ public void TestLineFragmentAbstraction() var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); - var line1 = wrappedLines[0]; + } [TestMethod] public void TestLineFragmentSeeWhatLinesUseWhatRichText() { - var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); var fragments = GetTextFragments(); @@ -46,10 +44,14 @@ public void TestLineFragmentSeeWhatLinesUseWhatRichText() var lines = wrappedCollection.GetTextLinesThatUse(fragments[4]); var specificFragments = wrappedCollection.GetLineFragmentsThatUse(fragments[4]); + var lineIndicies = wrappedCollection.GetLineNumbersThatUse(fragments[4]); - Assert.AreEqual(lines[0].InternalLineFragments[1], specificFragments[0]); + Assert.AreEqual(lines[0].LineFragments[1], specificFragments[0]); Assert.AreEqual(fragments[4], wrappedCollection.LineFragments[6].OriginalTextFragment); Assert.AreEqual(Color.DarkRed, wrappedCollection.LineFragments[6].OriginalTextFragment.RichTextOptions.FontColor); + + var expectedArr = new int[] { 3, 4 }; + expectedArr.SequenceCompareTo(lineIndicies); } List GetTextFragments() diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index 9551f6d53..95fce058d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -13,9 +13,30 @@ namespace EPPlus.Fonts.OpenType.Integration public class TextLineCollection : List, IEnumerable { - public List LineFragments = new List(); + /// + /// Array of the line numbers where fragment occurs + /// + public List LineFragments = new List(); List _originalFragments; + public int[] GetLineNumbersThatUse(TextFragment fragment) + { + var idx = _originalFragments.IndexOf(fragment); + List result = new List(); + if (idx != -1) + { + foreach (var key in fragIdLookup[idx].Keys) + { + result.Add(key); + } + return result.ToArray(); + } + else + { + return null; + } + } + /// /// Returns null if fragment is not found in any lines /// @@ -45,7 +66,7 @@ public List GetTextLinesThatUse(TextFragment fragment) /// /// /// - public List GetLineFragmentsThatUse(TextFragment fragment) + public List GetInternalLineFragmentsThatUse(TextFragment fragment) { var idx = _originalFragments.IndexOf(fragment); @@ -67,32 +88,32 @@ public List GetLineFragmentsThatUse(TextFragment fragment) return retFragments; } - ///// - ///// Returns null if fragment is not found in any linefragments - ///// - ///// - ///// - //public List GetLineFragmentDataThatUses(TextFragment fragment) - //{ - // var idx = _originalFragments.IndexOf(fragment); - - // List retFragments = null; - - // if (idx != -1) - // { - // retFragments = new List(); - - // foreach (var key in fragIdLookup[idx].Keys) - // { - // foreach (var lineFragment in fragIdLookup[idx][key]) - // { - // retFragments.Add(this[key].LineFragments[lineFragment]); - // } - // } - // } - - // return retFragments; - //} + /// + /// Returns null if fragment is not found in any linefragments + /// + /// + /// + public List GetLineFragmentsThatUse(TextFragment fragment) + { + var idx = _originalFragments.IndexOf(fragment); + + List retFragments = null; + + if (idx != -1) + { + retFragments = new List(); + + foreach (var key in fragIdLookup[idx].Keys) + { + foreach (var lineFragment in fragIdLookup[idx][key]) + { + retFragments.Add(this[key].LineFragments[lineFragment]); + } + } + } + + return retFragments; + } /// /// The id of the orginal fragment may correspond to @@ -108,6 +129,10 @@ internal MeasurementFont GetFont(int fragIdx) return _originalFragments[fragIdx].Font; } + /// + /// If using this MUST call FinalizeTextLineData to finish the information gathering + /// + /// public TextLineCollection(TextFragmentCollectionSimple fragmentCollection) { _originalFragments = fragmentCollection; @@ -140,12 +165,22 @@ internal void FinalizeTextLineData(List lines) { var idx = lf.FragmentIndex; AddToDictionary(idx, lineNum, fragCount); + LineFragmentOutput data; + if (lines[i].LineFragments == null || lines[i].LineFragments.Count != lines[i].InternalLineFragments.Count) + { + data = new LineFragmentOutput( + () => { return _originalFragments[idx]; }, + () => { return lf.Width; }, + () => { return lf.StartIdx; }, + lines[i].GetLineFragmentText(lf) + ); + } + else + { + //If already calculate don't duplicate just use the data + data = lines[i].LineFragments[fragCount]; + } - LineFragmentData data = new LineFragmentData( - () => { return _originalFragments[idx]; }, - () => { return lf.Width; }, - lines[i].GetLineFragmentText(lf), - lf.StartIdx); LineFragments.Add(data); fragCount++; @@ -163,35 +198,7 @@ public TextLineCollection(List lines, List origina fragIdLookup.Add(i, new Dictionary>()); } - for(int i = 0; i < lines.Count; i++) - { - int lineNum = i; - int fragCount = 0; - - //lines[i].CreateFinalizedSubstringsInLineFragments(); - - foreach (var lf in lines[i].InternalLineFragments) - { - var idx = lf.FragmentIndex; - - if (fragIdLookup[idx].ContainsKey(lineNum) == false) - { - fragIdLookup[idx].Add(lineNum, new List()); - } - - fragIdLookup[idx][lineNum].Add(fragCount); - - LineFragmentData data = new LineFragmentData( - () => { return _originalFragments[idx]; }, - () => { return lf.Width; }, - lines[i].GetLineFragmentText(lf), - lf.StartIdx); - LineFragments.Add(data); - - fragCount++; - } - Add(lines[i]); - } + FinalizeTextLineData(lines); } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index a5e884f5a..909256cdd 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -20,7 +20,7 @@ public class TextLineSimple /// /// External output data for reading /// - public List LineFragments { get; internal set; } = new List(); + public List LineFragments { get; internal set; } = new List(); public string Text { get; internal set; } /// @@ -102,11 +102,12 @@ internal void FinalizeLineFragments(List originalFragments) { foreach (var lf in InternalLineFragments) { - LineFragmentData data = new LineFragmentData( + LineFragmentOutput data = new LineFragmentOutput( () => { return originalFragments[lf.FragmentIndex]; }, () => { return lf.Width; }, - GetLineFragmentText(lf), - lf.StartIdx); + () => { return lf.StartIdx; }, + GetLineFragmentText(lf) + ); LineFragments.Add(data); } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index ee7f18e62..cdb8d7a2f 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -29,32 +29,10 @@ public class LineFragment /// public double SpaceWidth { get; internal set; } - //public string Text { get { return _finalFragmentText; } } - - ////Seeing duplicated string in debugger creates clutter - ////Better to check Text and then realise the private variable is the actual value holder - //[DebuggerBrowsable(DebuggerBrowsableState.Never)] - //private string _finalFragmentText; - - ///// - ///// Only to be set after finishing all operations - ///// Only to be set by TextLineSimple - ///// This class should be essentially read only after setting this - ///// - ///// - //internal void SetFinalizedText(string text) - //{ - // _finalFragmentText = text; - //} - //public double AscentInPoints { get; private set; } - //public double DescentInPoints { get; private set; } - - internal LineFragment(int rtFragmentIdx, int idxWithinLine/*, double ascentInPoints, double descentInPoints*/) + internal LineFragment(int rtFragmentIdx, int idxWithinLine) { FragmentIndex = rtFragmentIdx; StartIdx = idxWithinLine; - //AscentInPoints = ascentInPoints; - //DescentInPoints = descentInPoints; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs similarity index 55% rename from src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs rename to src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index b241d4dfd..7458c808d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentData.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -7,12 +7,12 @@ namespace EPPlus.Fonts.OpenType.Integration { [DebuggerDisplay("{Text}")] - public class LineFragmentData + public class LineFragmentOutput { /// /// Char idx within the line /// - public int StartIdx { get; } + public int StartIdx { get { return _getStartIdx(); } } /// /// Width of this fragment /// @@ -20,26 +20,34 @@ public class LineFragmentData /// /// Text of this fragment + /// Is not a callback since the LineFragment class does not store the string data directly. + /// This is for performance reasons. /// public string Text { get; } /// /// Original text fragment + /// If this becomes null/loses its reference the original textfragment has been deleted + /// Don't delete/clear the original textFragment list if you plan to use/check it here. /// public TextFragment OriginalTextFragment { get { return _getTextFragment(); } } + /// + /// Looks up the data in the internal class LineFragment instead of making copies + /// #region Callbacks Func _getTextFragment; Func _getWidth; + Func _getStartIdx; #endregion - internal LineFragmentData(Func getTextFragment, Func getWidth, string text, int startidx) + internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx ,string text) { _getTextFragment = getTextFragment; _getWidth = getWidth; + _getStartIdx = startIdx; Text = text; - StartIdx = startidx; } } } From 00a279a245b5c52404bb55f6dabba4a7a1967c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 22 Apr 2026 16:48:38 +0200 Subject: [PATCH 09/26] Cleaned up unused classes + added comments --- .../Integration/DataHolders/CharInfo.cs | 1 + .../Integration/DataHolders/GlyphRect.cs | 28 -- .../DataHolders/IRichTextInfoBase.cs | 3 + .../DataHolders/LineFragmentCollection.cs | 91 ---- .../DataHolders/RichTextFragmentSimple.cs | 46 -- .../DataHolders/TextFragmentAdvanced.cs | 78 ---- .../TextFragmentCollectionAdvanced.cs | 415 ------------------ .../Integration/DataHolders/TextLineSimple.cs | 64 --- .../Integration/DataHolders/TextParagraph.cs | 83 ---- .../Integration/FragmentPosition.cs | 28 -- .../Integration/LineFragmentOutput.cs | 3 + .../Integration/TextFragment.cs | 3 + .../Integration/TextRun.cs | 27 -- .../Integration/WrappedLine.cs | 25 -- 14 files changed, 10 insertions(+), 885 deletions(-) delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/GlyphRect.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/RichTextFragmentSimple.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextParagraph.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/TextRun.cs delete mode 100644 src/EPPlus.Fonts.OpenType/Integration/WrappedLine.cs diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs index c6ddae562..599fa5e3c 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs @@ -4,6 +4,7 @@ using System.Text; namespace EPPlus.Fonts.OpenType.Integration { + //Leaving this for now. May be neccesary for vertical text internal class CharInfo { internal int Index; diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/GlyphRect.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/GlyphRect.cs deleted file mode 100644 index cf53f8d99..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/GlyphRect.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration -{ - internal class GlyphRect - { - //BoundingRectangle BoundingRectFontDesign; - ushort glyphIndex; - internal double advanceWidth; - - //Debug var only? - string fontName; - - internal GlyphRect(ushort glyphIndex, double advanceWidth, string fontName) - { - this.glyphIndex = glyphIndex; - this.advanceWidth = advanceWidth; - this.fontName = fontName; - } - //BoundingRectangle CalculateBoundingRect(double fontSize) - //{ - - //} - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs index 12ad97970..2206e1add 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs @@ -8,6 +8,9 @@ namespace EPPlus.Fonts.OpenType.Integration.DataHolders { + /// + /// Interface for pdf/svg/future richtext users to unify richtext styling + /// public interface IRichTextInfoBase { diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs deleted file mode 100644 index 2e22dec58..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/LineFragmentCollection.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration.DataHolders -{ - public class LineFragmentCollection : List - { - private string _text; - - public string FullText - { - get { return _text; } - internal set { RichTextSubstrings.Clear(); _text = value; } - } - - internal List RichTextSubstrings { get; private set; } - - - - public LineFragmentCollection(string originalText) - { - FullText = originalText; - } - - //Note: Array size never intended to be larger than 2 - List StartEndPerFragment = new List(); - - - /// - /// Logs start and end idx per fragment - /// Then adds fragment as regular list - /// - /// - public new void Add(LineFragment fragment) - { - var endIdx = FullText.Length - 1; - if (Count != 0) - { - StartEndPerFragment.Last()[1] = fragment.StartIdx; - } - - StartEndPerFragment.Add(new int[] { fragment.StartIdx, endIdx}); - base.Add(fragment); - } - - public string GetLineFragmentText(LineFragment rtFragment) - { - if (this.Contains(rtFragment) == false) - { - throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); - } - - if (string.IsNullOrEmpty(FullText)) - { - return FullText; - } - - var startIdx = rtFragment.StartIdx; - - var idxInLst = this.FindIndex(x => x == rtFragment); - if (idxInLst == this.Count - 1) - { - return FullText.Substring(startIdx, FullText.Length - startIdx); - } - else - { - var endIdx = this[idxInLst + 1].StartIdx; - return FullText.Substring(startIdx, endIdx - startIdx); - } - } - - internal void GenerateSubstrings() - { - RichTextSubstrings.Clear(); - - //for (int i = 0; i < StartEndPerFragment.Count; i++) - //{ - // var startIdx = StartEndPerFragment[i][0]; - // var endIdx = StartEndPerFragment[i][1]; - - // RichTextSubstrings.Add(FullText[1..5]); - //} - for (int i = 0; i < Count; i++) - { - RichTextSubstrings.Add(GetLineFragmentText(this[i])); - } - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/RichTextFragmentSimple.cs deleted file mode 100644 index 7f77f0cc6..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/RichTextFragmentSimple.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration -{ - public class RichTextFragmentSimple - { - public int Fragidx { get; internal set; } - - internal int OverallParagraphStartCharIdx { get; set; } - //Char length - internal int charStarIdxWithinCurrentLine { get; set; } - /// - /// Width in points - /// - public double Width { get; internal set; } - - public RichTextFragmentSimple() - { - Width = 0; - Fragidx = 0; - OverallParagraphStartCharIdx = 0; - charStarIdxWithinCurrentLine = 0; - } - - //public RichTextFragmentSimple(int fragIdx, ) - //{ - // Width = 0; - // Fragidx = 0; - // OverallParagraphStartCharIdx = 0; - // charStarIdxWithinCurrentLine = 0; - //} - - internal RichTextFragmentSimple Clone() - { - var fragment = new RichTextFragmentSimple(); - fragment.Width = Width; - fragment.OverallParagraphStartCharIdx = OverallParagraphStartCharIdx; - fragment.charStarIdxWithinCurrentLine = charStarIdxWithinCurrentLine; - fragment.Fragidx = Fragidx; - return fragment; - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs deleted file mode 100644 index 2879bda9f..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentAdvanced.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Fonts.OpenType.Integration.DataHolders -{ - //TextFragment is a portion of a longer string - //the fragment itself may contain line-breaks - internal class TextFragmentAdvanced - { - /// - /// Char index starting position for this fragment - /// - internal int StartPos; - - internal List IsPartOfLines; - internal string ThisFragment; - internal List PointWidthPerOutputLineInFragment = new(); - internal List PointYIncreasePerLine = new(); - - List positionsToBreakAt = new(); - - internal float FontSize { get; set; } = float.NaN; - - internal TextFragmentAdvanced(string thisFragment, int startPos) - { - ThisFragment = thisFragment; - StartPos = startPos; - } - - internal int GetEndPosition() - { - return StartPos + ThisFragment.Length; - } - - /// - /// Line-breaks to be added after wrapping - /// - /// - internal void AddLocalLbPositon(int pos) - { - var locaLnPos = pos - StartPos; - positionsToBreakAt.Add(locaLnPos); - } - - internal string GetLineBreakFreeFragment() - { - string freeFragment = ThisFragment.Replace("\r", ""); - freeFragment.Replace("\n", ""); - return freeFragment; - } - - internal string GetWrappedFragment() - { - if (positionsToBreakAt.Count == 0) - { - return ThisFragment; - } - - string alteredFragment = ThisFragment; - int deleteSpaceCount = 0; - for (int i = 0; i < positionsToBreakAt.Count; i++) - { - var insertPosition = positionsToBreakAt[i] + i - deleteSpaceCount; - alteredFragment = alteredFragment.Insert(insertPosition, Environment.NewLine); - //Spaces after inserted newlines are to be removed - if (alteredFragment[insertPosition + Environment.NewLine.Length] == ' ') - { - alteredFragment = alteredFragment.Remove(insertPosition + Environment.NewLine.Length, 1); - deleteSpaceCount++; - } - } - - return alteredFragment; - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs deleted file mode 100644 index 16501d1a7..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollectionAdvanced.cs +++ /dev/null @@ -1,415 +0,0 @@ -using EPPlus.Fonts.OpenType.Integration.DataHolders; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Xml; - -namespace EPPlus.Fonts.OpenType.Integration -{ - public class TextFragmentCollectionAdvanced - { - internal Dictionary CharLookup = new(); - - internal List TextFragments = new(); - - internal string AllText; - internal List AllTextNewLineIndicies = new(); - List _fragmentItems = new List(); - List _lines = new List(); - List outputFragments = new List(); - //List outputStrings = new List(); - internal List LargestFontSizePerLine { get; private set; } = new(); - internal List AscentPerLine { get; private set; } = new(); - internal List DescentPerLine { get; private set; } = new(); - - /// - /// Added linewidths in points - /// - internal List lineWidths { get; private set; } = new List(); - - public List IndiciesToWrapAt { get; internal set; } - - List TextLines = new(); - - public TextFragmentCollectionAdvanced(List textFragments) - { - TextFragments = textFragments; - IndiciesToWrapAt = new List(); - - //Set data for each fragment and for AllText - var currentTotalLength = 0; - for (int i = 0; i < textFragments.Count; i++) - { - var currString = textFragments[i]; - var fragment = new TextFragmentAdvanced(currString, currentTotalLength); - currentTotalLength += currString.Length; - _fragmentItems.Add(fragment); - } - - AllText = string.Join(string.Empty, textFragments.ToArray()); - - //Get the indicies where newlines occur in the combined string - AllTextNewLineIndicies = GetFirstCharPositionOfNewLines(AllText); - - //Save minor information about each char so each char knows its line/fragment - int charCount = 0; - int lineIndex = 0; - - List currFragments = new(); - int lineStartCharIndex = 0; - - //For each fragment - for (int i = 0; i < TextFragments.Count; i++) - { - var textFragment = TextFragments[i]; - - //For each char in current fragment - for (int j = 0; j < textFragment.Length; j++) - { - if (lineIndex <= AllTextNewLineIndicies.Count - 1 && - charCount >= AllTextNewLineIndicies[lineIndex]) - { - var text = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); - var trimmedText = text.Trim(['\r', '\n']); - - var line = new TextLineSimple(); - line.Text = trimmedText; - - foreach (var idx in currFragments) - { - var fragment = new LineFragment(i, idx); - } - - lineStartCharIndex = AllTextNewLineIndicies[lineIndex]; - _lines.Add(line); - currFragments.Clear(); - lineIndex++; - } - - var info = new CharInfo(charCount, i, lineIndex); - CharLookup.Add(charCount, info); - charCount++; - } - currFragments.Add(i); - } - - //Add the last line - var lastText = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); - var lastTrimmedText = lastText.Trim(['\r', '\n']); - - var lastLine = new TextLineSimple(); - lastLine.Text = lastTrimmedText; - - foreach (var idx in currFragments) - { - var fragment = new LineFragment(idx, lineStartCharIndex); - } - - _lines.Add(lastLine); - currFragments.Clear(); - } - - public TextFragmentCollectionAdvanced(List textFragments, List fontSizes) : this(textFragments) - { - if (textFragments.Count() != fontSizes.Count) - { - throw new InvalidOperationException($"TextFragment list and FontSizes list must be equal." + - $"Counts:" + - $"textFragment: {textFragments.Count()}" + - $"fontSizes: {fontSizes.Count()}"); - } - - for (int i = 0; i < textFragments.Count; i++) - { - _fragmentItems[i].FontSize = fontSizes[i]; - } - } - - private List GetFirstCharPositionOfNewLines(string stringsCombined) - { - List positions = new List(); - for (int i = 0; i < stringsCombined.Length; i++) - { - if (stringsCombined[i] == '\n') - { - if (i < stringsCombined.Length - 1) - { - positions.Add(i + 1); - } - else - { - positions.Add(i); - } - } - } - return positions; - } - - internal void AddLargestFontSizePerLine(double fontSize) - { - LargestFontSizePerLine.Add(fontSize); - } - - internal void AddAscentPerLine(double Ascent) - { - AscentPerLine.Add(Ascent); - } - - internal void AddDescentPerLine(double Descent) - { - DescentPerLine.Add(Descent); - } - - internal void AddWrappingIndex(int index) - { - IndiciesToWrapAt.Add(index); - } - - public List GetFragmentsWithFinalLineBreaks() - { - //var newFragments = TextFragments.ToArray(); - foreach (var i in IndiciesToWrapAt) - { - _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); - } - - foreach (var fragment in _fragmentItems) - { - outputFragments.Add(fragment.GetWrappedFragment()); - } - - return outputFragments; - } - - public List GetOutputLines() - { - var fragments = GetFragmentsWithFinalLineBreaks(); - - string outputAllText = ""; - - //Create final all text - for (int i = 0; i < fragments.Count(); i++) - { - outputAllText += fragments[i]; - } - - var newLineIndiciesOutput = GetFirstCharPositionOfNewLines(outputAllText); - List outputTextLines = new List(); - - //Save minor information about each char so each char knows its line/fragment - int charCount = 0; - int lineIndex = 0; - - List currFragments = new(); - int lineStartCharIndex = 0; - int RtInternalStartIndex = 0; - - //For each fragment - for (int i = 0; i < fragments.Count; i++) - { - var textFragment = fragments[i]; - - //For each char in current fragment - for (int j = 0; j < textFragment.Length; j++) - { - if (lineIndex <= newLineIndiciesOutput.Count - 1 && - charCount >= newLineIndiciesOutput[lineIndex]) - { - //We have reached a new line - var text = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); - var trimmedText = text.Trim(['\r', '\n']); - - var line = new TextLineSimple(); - line.Text = trimmedText; - - foreach (var idx in currFragments) - { - var fragment = new LineFragment(i, idx); - } - - - //var line = new TextLine() - //{ - // richTextIndicies = currFragments, - // content = trimmedText, - // startIndex = lineStartCharIndex, - // rtContentStartIndexPerRt = new List(j), - // lastRtInternalIndex = j, - // startRtInternalIndex = RtInternalStartIndex - //}; - - lineStartCharIndex = newLineIndiciesOutput[lineIndex]; - outputTextLines.Add(line); - currFragments.Clear(); - RtInternalStartIndex = j; - lineIndex++; - } - - //var info = new CharInfo(charCount, i, lineIndex); - //CharLookup.Add(charCount, info); - charCount++; - } - currFragments.Add(i); - } - - //Add the last line - var lastText = AllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); - var lastTrimmedText = lastText.Trim(['\r', '\n']); - - var lastLine = new TextLineSimple(); - lastLine.Text = lastTrimmedText; - - foreach (var idx in currFragments) - { - var fragment = new LineFragment(lineStartCharIndex, idx); - } - - _lines.Add(lastLine); - currFragments.Clear(); - - ////Add the last line - //var lastText = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); - //var lastTrimmedText = lastText.Trim(['\r', '\n']); - - //var lastLine = new TextLine() - //{ - // richTextIndicies = currFragments, - // content = lastTrimmedText, - // startIndex = lineStartCharIndex, - // lastRtInternalIndex = fragments.Last().Length, - // startRtInternalIndex = RtInternalStartIndex - //}; - - //outputTextLines.Add(lastLine); - //currFragments.Clear(); - - return outputTextLines; - } - - //public void GetTextFragmentOutputStartIndiciesOnFinalLines(List finalOutputLines) - //{ - // var lineBreakStrings = GetFragmentsWithFinalLineBreaks(); - // //var fragmentsWithoutLineBreaks = GetFragmentsWithoutLineBreaks(); - // string outputAllText = ""; - - // //List> eachFragmentForEachLine = new List>(); - - // for(int i = 0; i< finalOutputLines.Count(); i++) - // { - // var line = new TextLine(); - // line.content = finalOutputLines[i]; - - // } - - // //Create final all text - // for(int i = 0; i< lineBreakStrings.Count(); i++) - // { - - // ////var length = lineBreakStrings[i].Length; - // //outputAllText += lineBreakStrings[i]; - // } - - - //} - - //public List GetSimpleTextLines(List wrappedLines) - //{ - // //The wrapped lines - // var lines = wrappedLines; - // //The richText data in a form that is one to one in chars - // var frags = GetFragmentsWithoutLineBreaks(); - - // int fragIdx = 0; - // int charIdx = 0; - // int lastFragLength = 0; - - // var currentFragment = frags[fragIdx]; - - // for (int i = 0; i< wrappedLines.Count(); i++) - // { - // var textLineSimple = new TextLineSimple(); - - // for (int j = 0; j < wrappedLines[i].Length; j++) - // { - // textLineSimple - // charIdx++; - // } - // } - //} - - public List GetFragmentsWithoutLineBreaks() - { - ////var newFragments = TextFragments.ToArray(); - //foreach (var i in IndiciesToWrapAt) - //{ - // _fragmentItems[CharLookup[i].Fragment].AddLocalLbPositon(i); - //} - - foreach (var fragment in _fragmentItems) - { - outputFragments.Add(fragment.GetLineBreakFreeFragment()); - } - - return outputFragments; - } - - internal void AddLineWidth(double width) - { - lineWidths.Add(width); - } - - /// - /// Should arguably be done in constructor instead of created every time - /// - /// - /// - public List GetLargestFontSizesOfEachLine() - { - //List largestFontSizes = new List(); - //foreach(var line in _lines) - //{ - // float largest = float.MinValue; - // foreach(var idx in line.richTextIndicies) - // { - // if (float.IsNaN(_fragmentItems[idx].FontSize) == false && _fragmentItems[idx].FontSize > largest) - // { - // largest = _fragmentItems[idx].FontSize; - // } - // } - // if (largest != float.MinValue) - // { - // largestFontSizes.Add(largest); - // } - //} - //return largestFontSizes; - return LargestFontSizePerLine; - } - - public double GetAscent(int lineIdx) - { - return AscentPerLine[lineIdx]; - } - - public double GetDescent(int lineIdx) - { - return DescentPerLine[lineIdx]; - } - - internal void AddFragmentWidth(int fragmentIdx, double width) - { - _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment.Add(width); - } - - public List GetFragmentWidths(int fragmentIdx) - { - return _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment; - } - - public List GetLinesFragmentIsPartOf(int fragmentIndex) - { - //Add throw if index does not exist? - return _fragmentItems[fragmentIndex].IsPartOfLines; - } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 909256cdd..1e5367e66 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -8,7 +8,6 @@ namespace EPPlus.Fonts.OpenType.Integration { - //[DebuggerTypeProxy(typeof(TextLineSimpleVizualizer))] [DebuggerDisplay("{Text}")] public class TextLineSimple { @@ -78,26 +77,6 @@ public TextLineSimple() { } - public TextLineSimple(string text, double largestFontSize, double largestAscent, double largestDescent) - { - InternalLineFragments = new LineFragmentCollection(text); - LargestFontSize = largestFontSize; - LargestAscent = largestAscent; - LargestDescent = largestDescent; - } - - ///// - ///// Inserts the relevant substrings directly into the line fragments - ///// - //internal void CreateFinalizedSubstringsInLineFragments() - //{ - // foreach (var lineFragment in InternalLineFragments) - // { - // var text = GetLineFragmentText(lineFragment); - // lineFragment.SetFinalizedText(text); - // } - //} - internal void FinalizeLineFragments(List originalFragments) { foreach (var lf in InternalLineFragments) @@ -149,47 +128,4 @@ internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, d return newLineFragment; } } - - //internal class TextLineSimpleVizualizer - //{ - // public List Display - // { - - // get - // { - // List startIndices = new List(); - // List lines = new List(); - - // foreach(var fragment in _content.LineFragments ) - // { - // //startIndices.Add(fragment.StartIdx); - // lines.Add(_content.GetLineFragmentText(fragment)+$" rtIdx:{fragment.RtFragIdx}"); - // } - - // //startIndices.Add(_content.Text.Length -1); - - // //List lines = new List(); - - - // //for (int i = 0; i < startIndices.Count -1; i++) - // //{ - // // var startidx = startIndices[i + 1]+1; - // // var length = startidx - startIndices[i]; - // // var substring = _content.Text.Substring(startIndices[i], length); - // // lines.Add(substring); - // //} - - - - // return lines; - // } - // } - - // private TextLineSimple _content; - - // public TextLineSimpleVizualizer(TextLineSimple content) - // { - // _content = content; - // } - //} } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextParagraph.cs deleted file mode 100644 index aa1bc9c71..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextParagraph.cs +++ /dev/null @@ -1,83 +0,0 @@ -//using EPPlus.Fonts.OpenType.Tables.Cmap; -//using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings; -//using OfficeOpenXml.Interfaces.Drawing.Text; -//using OfficeOpenXml.Interfaces.Fonts; -//using System; -//using System.Collections; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; - -//namespace EPPlus.Fonts.OpenType.Integration -//{ -// public class TextParagraph -// { -// //prevent creating multiple OpenTypeFonts via cache/indexing -// internal Dictionary FontIndexDict = new(); -// internal Dictionary GlyphMappings = new(); -// internal int TotalLength = 0; - -// internal List FontSizes = new(); - -// internal TextFragmentCollection Fragments; - -// public TextParagraph(TextFragmentCollection fragments, List fonts) -// { -// List openTypeFonts = new List(); -// FontSizes = new List(); -// Fragments = fragments; - -// var distinctFonts = fonts.Distinct().ToArray(); - -// //Collect fonts that are actually distinct -// foreach (var distinctFont in distinctFonts) -// { -// var subFont = GetFontSubType(distinctFont.Style); -// var font = GetFont(distinctFont.FontFamily, subFont); -// openTypeFonts.Add(font); -// } - -// //Setup lookup for different properties -// for (int i = 0; i < fonts.Count; i++) -// { -// for (int j = 0; j < distinctFonts.Count(); j++) -// { -// if (fonts[i] == distinctFonts[j]) -// { -// FontIndexDict.Add(i, openTypeFonts[j]); -// GlyphMappings.Add(i, openTypeFonts[j].CmapTable.GetPreferredSubtable().GetGlyphMappings()); -// } -// } -// FontSizes.Add(fonts[i].Size); -// } -// } - -// public TextParagraph(List textFragment, List fontSizes, Dictionary fontIndexDict) -// { - -// } - -// OpenTypeFont GetFont(string fontName, FontSubFamily subFamily) -// { -// return OpenTypeFonts.LoadFont(fontName, subFamily); -// } - -// private FontSubFamily GetFontSubType(MeasurementFontStyles Style) -// { -// if ((Style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) -// { -// return FontSubFamily.BoldItalic; -// } -// else if ((Style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold) -// { -// return FontSubFamily.Bold; -// } -// else if ((Style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic) -// { -// return FontSubFamily.Italic; -// } - -// return FontSubFamily.Regular; -// } -// } -//} diff --git a/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs b/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs deleted file mode 100644 index 73c5734d2..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/FragmentPosition.cs +++ /dev/null @@ -1,28 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 01/20/2025 EPPlus Software AB TextLayoutEngine implementation - *************************************************************************************************/ -using OfficeOpenXml.Interfaces.Drawing.Text; -using OfficeOpenXml.Interfaces.Fonts; - -namespace EPPlus.Fonts.OpenType.Integration -{ - /// - /// Internal class to track fragment positions in the full text. - /// - internal class FragmentPosition - { - public int StartIndex { get; set; } - public int EndIndex { get; set; } - public MeasurementFont Font { get; set; } - public ShapingOptions Options { get; set; } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index 7458c808d..9b47269ea 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -6,6 +6,9 @@ namespace EPPlus.Fonts.OpenType.Integration { + /// + /// Finalized output which uses callbacks to get data but can never have data set. + /// [DebuggerDisplay("{Text}")] public class LineFragmentOutput { diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs index 4f55773ba..aef9ae09b 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs @@ -38,6 +38,9 @@ public class TextFragment public double DescentPoints { get; set; } } + /// + /// Simple class to provide some kind of fallback/defaults + /// public class RichTextDefaults : IRichTextInfoBase { internal RichTextDefaults() diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextRun.cs b/src/EPPlus.Fonts.OpenType/Integration/TextRun.cs deleted file mode 100644 index 869b2b07c..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/TextRun.cs +++ /dev/null @@ -1,27 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 01/20/2025 EPPlus Software AB TextRun implementation - *************************************************************************************************/ -using OfficeOpenXml.Interfaces.Drawing.Text; - -namespace EPPlus.Fonts.OpenType.Integration -{ - /// - /// Represents a portion of text with consistent formatting. - /// - public class TextRun - { - public string Text { get; set; } - public MeasurementFont Font { get; set; } - public int StartIndex { get; set; } - public int Length { get; set; } - } -} diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrappedLine.cs b/src/EPPlus.Fonts.OpenType/Integration/WrappedLine.cs deleted file mode 100644 index 08320da41..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/WrappedLine.cs +++ /dev/null @@ -1,25 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 01/20/2025 EPPlus Software AB WrappedLine implementation - *************************************************************************************************/ -using System.Collections.Generic; - -namespace EPPlus.Fonts.OpenType.Integration -{ - /// - /// Represents a wrapped line with rich text information. - /// - public class WrappedLine - { - public string Text { get; set; } - public List Runs { get; set; } - } -} From aa8845d4e7b66e954dfc2a9a5c2509543f4e81dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 27 Apr 2026 16:08:43 +0200 Subject: [PATCH 10/26] Fixed bug where every char has richtext while wrapping on space --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 85 ++++++++++++++++++- .../DataHolders/TextLineSimpleTests.cs | 1 + .../Integration/DataHolders/TextLineSimple.cs | 1 + .../Integration/WrapStateRichText.cs | 13 ++- 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index c6fe88cf2..414c6bc80 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -10,14 +10,19 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf; +using EPPlus.Export.Pdf.PdfLayout; +using EPPlus.Export.Pdf.PdfSettings; +using EPPlus.Export.Pdf.PdfSettings.PdfPageSizes; +using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Integration; +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Style; using System; using System.IO; using System.Linq; using System.Text; using System.Xml.Linq; -using EPPlus.Export.Pdf; -using EPPlus.Export.Pdf.PdfSettings; -using EPPlus.Export.Pdf.PdfSettings.PdfPageSizes; namespace EPPlusTest.PDF { @@ -125,5 +130,79 @@ public void TestWritePdf2() ExcelPdf pedeef = new ExcelPdf(p.Workbook.Worksheets.First(), pageSettings); pedeef.CreatePdf("c:\\epplustest\\pdf\\EmojiTest.pdf"); } + + [TestMethod] + public void WritePrintAreas() + { + using var p = OpenTemplatePackage("PDFTest.xlsx"); + var cell = p.Workbook.Worksheets[0].Cells["P118"]; + + List TextFragments = GetTextFragments(cell.RichText); + + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(TextFragments[0].Font); + + var TextLines = layout.WrapRichTextLineCollection(TextFragments, 51d); + ////var ShapedTexts = new List(); + //for (int i = 0; i < TextFragments.Count; i++) + //{ + // var tf = TextFragments[i]; + // var layout = OpenTypeFonts.GetTextLayoutEngineForFont(TextFragments[i].Font); + + // var TextLines = layout.WrapRichTextLineCollection(TextFragments, 51d); + //} + + } + + private static List GetTextFragments(ExcelRichTextCollection RichTextCollection, PdfCellStyle cellStyle = null) + { + var textFragments = new List(); + bool bold = false, italic = false, underline = false, strike = false; + ExcelUnderLineType underLineType = ExcelUnderLineType.None; + if (cellStyle != null && cellStyle.dxfFont != null) + { + bold = cellStyle.dxfFont.Bold != null ? (bool)cellStyle.dxfFont.Bold : false; + italic = cellStyle.dxfFont.Italic != null ? (bool)cellStyle.dxfFont.Italic : false; + strike = cellStyle.dxfFont.Strike != null ? (bool)cellStyle.dxfFont.Strike : false; + underline = cellStyle.dxfFont.Underline != null; + underLineType = cellStyle.dxfFont.Underline != null ? (ExcelUnderLineType)cellStyle.dxfFont.Underline : ExcelUnderLineType.None; + } + for (int i = 0; i < RichTextCollection.Count; i++) + { + var rt = RichTextCollection[i]; + var textFrag = new TextFragment(); + textFrag.Font = new MeasurementFont(); + textFrag.Text = rt.Text; + + textFrag.Font.FontFamily = rt.FontName; + textFrag.Font.Size = rt.Size; + + textFrag.RichTextOptions.IsBold = rt.Bold || bold; + textFrag.RichTextOptions.IsItalic = rt.Italic || italic; + //underline + //none : 12 + //single : 13 + //Double : 4 + //accouting does not exsist + textFrag.RichTextOptions.UnderlineType = 12; + textFrag.RichTextOptions.UnderlineType = rt.UnderLineType == ExcelUnderLineType.Single ? 13 : textFrag.RichTextOptions.UnderlineType; + textFrag.RichTextOptions.UnderlineType = rt.UnderLineType == ExcelUnderLineType.Double ? 4 : textFrag.RichTextOptions.UnderlineType; + textFrag.RichTextOptions.StrikeType = rt.Strike || strike ? 2 : 1; + textFrag.RichTextOptions.SuperScript = rt.VerticalAlign == ExcelVerticalAlignmentFont.Superscript; + textFrag.RichTextOptions.SubScript = rt.VerticalAlign == ExcelVerticalAlignmentFont.Subscript; + textFrag.RichTextOptions.FontColor = rt.Color; + + textFrag.Font.Style = (textFrag.RichTextOptions.IsBold ? MeasurementFontStyles.Bold : 0) | + (textFrag.RichTextOptions.IsItalic ? MeasurementFontStyles.Italic : 0) | + (textFrag.RichTextOptions.UnderlineType != 12 ? MeasurementFontStyles.Underline : 0) | + (textFrag.RichTextOptions.StrikeType > 1 ? MeasurementFontStyles.Strikeout : 0); + + + textFragments.Add(textFrag); + OpenTypeFonts.GetFontSubFamily(textFrag.Font.Style); + } + + return textFragments; + } } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs index ca683ee1c..5472bea66 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -2,6 +2,7 @@ using EPPlus.Fonts.OpenType.TextShaping; using EPPlus.Fonts.OpenType.Utils; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Style; using System; using System.Collections.Generic; using System.Drawing; diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 1e5367e66..eeebb78d4 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -122,6 +122,7 @@ internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, d //If we are splitting a fragment its position in the new line should be 0 var newLineFragment = new LineFragment(origLf.FragmentIndex, 0); newLineFragment.Width = origLf.Width - widthAtSplit; + newLineFragment.SpaceWidth = origLf.SpaceWidth; origLf.Width = widthAtSplit; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 907b5ddad..3bf3528ef 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -104,6 +104,11 @@ internal void AdjustLineFragmentsForNextLine() //Iterate backwards from back of list until we hit fragment for (int i = CurrentTextLine.InternalLineFragments.Count()-1; i > _listIdxWithinLine; i--) { + //The current fragment's startidx is affected by the split if it is not the first in new line + if (CurrentTextLine.InternalLineFragments[i].StartIdx != 0) + { + CurrentTextLine.InternalLineFragments[i].StartIdx -= WordStart + 1; + } //Add fragment to the new list _fragmentsForNextLine.Insert(0, CurrentTextLine.InternalLineFragments[i]); //Remove it from the old @@ -112,8 +117,12 @@ internal void AdjustLineFragmentsForNextLine() if (_rtIdxAtWordStart != CurrentFragmentIdx) { - //We also insert the fragment we've split out - _fragmentsForNextLine.Insert(0, resultingFragment); + if (resultingFragment.Width != 0) + { + //We also insert the fragment we've split out + //Unless the leftover is an empty fragment (this happens when we wrap on a space that had to be trimmed) + _fragmentsForNextLine.Insert(0, resultingFragment); + } //The current fragment's startidx is affected by the split if it is not the first in new line if (LineFrag.StartIdx != 0) { From 4aefa27e4a4515a7fb9ca288fdc9f0906fa57fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 27 Apr 2026 16:53:46 +0200 Subject: [PATCH 11/26] Adjusted old test --- .../TextboxTest.cs | 17 +++++++++++++---- .../RenderItems/Shared/ParagraphItem.cs | 8 ++++++++ .../Integration/DataHolders/TextLineSimple.cs | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs index 757f95f48..2dc8523aa 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs @@ -49,15 +49,24 @@ public void TextBoxVerification() item.ExternalRenderItemsNoBounds.Add(txtBoxNotMaxed); + var sb = new StringBuilder(); + + item.Render(sb); + var svgString = sb.ToString(); + + //before we assumed we consider the space widths + var widthWithSpace = txtBox.TextBody.Paragraphs[0].SpaceWidthsPerLine[0] + txtBox.Width; + + //Assert.AreEqual(36d, txtBox.TextBody.Width); - Assert.AreEqual(37.5d, txtBox.Width, 0.5); + Assert.AreEqual(37.5d, widthWithSpace, 0.5); Assert.AreNotEqual(36d, txtBoxNotMaxed.Width); Assert.AreEqual(16.29052734375d, txtBoxNotMaxed.Width); - var sb = new StringBuilder(); + //var sb = new StringBuilder(); - item.Render(sb); - var svgString = sb.ToString(); + //item.Render(sb); + //var svgString = sb.ToString(); SaveTextFileToWorkbook($"svg\\StandAloneTextBox.svg", svgString); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index b5f32f34c..7c4b4ae21 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -46,6 +46,8 @@ internal abstract class ParagraphItem : RenderItem private double? _centerAdjustment = null; + internal List SpaceWidthsPerLine = new List(); + public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { ParentTextBody = textBody; @@ -294,12 +296,16 @@ internal void AddLinesAndTextRuns(string textIfEmpty) //START var idxOfLargestLine = 0; double widthOfLargestLine = lines[0].Width; + //SpaceWidthsPerLine.Add(lines[0].lastFontSpaceWidth); + for (int i = 1; i < lines.Count; i++) { if (lines[i].Width > widthOfLargestLine) { var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces(); + SpaceWidthsPerLine.Add(lines[i].lastFontSpaceWidth); + widthOfLargestLine = ctrLineWidth; idxOfLargestLine = i; } @@ -408,12 +414,14 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) //START var idxOfLargestLine = 0; double widthOfLargestLine = lines[0].GetWidthWithoutTrailingSpaces(); + //SpaceWidthsPerLine.Add(lines[0].lastFontSpaceWidth); for (int i = 1; i < lines.Count; i++) { if (lines[i].Width > widthOfLargestLine) { var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces(); + SpaceWidthsPerLine.Add(lines[i].lastFontSpaceWidth); widthOfLargestLine = ctrLineWidth; idxOfLargestLine = i; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index eeebb78d4..9c0d4abb1 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -122,6 +122,7 @@ internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, d //If we are splitting a fragment its position in the new line should be 0 var newLineFragment = new LineFragment(origLf.FragmentIndex, 0); newLineFragment.Width = origLf.Width - widthAtSplit; + newLineFragment.SpaceWidth = origLf.SpaceWidth; origLf.Width = widthAtSplit; From cd2a5f37280060cfda1bfff906a482d9ebbd12a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 30 Apr 2026 16:35:34 +0200 Subject: [PATCH 12/26] Added original position index --- .../Integration/TextLayoutEngineTests.cs | 26 +++++++++++++++++++ .../EPPlus.Fonts.OpenType.csproj | 4 +++ .../Integration/DataHolders/TextLineSimple.cs | 4 +-- .../Integration/LineFragment.cs | 7 ++++- .../Integration/TextLayoutEngine.RichText.cs | 6 +++-- .../Integration/WrapStateRichText.cs | 6 +++-- 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index c560fe1f1..934fdf29b 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -212,6 +212,32 @@ public void WrapText_WithKerning_MeasuresCorrectly() #region Rich Text Wrapping Tests + [TestMethod] + public void ImportingFromCells() + { + + } + + [TestMethod] + public void MyVeryGoodRichTextWrapper() + { + var font = OpenTypeFonts.LoadFont("Calibri", FontSubFamily.Regular, FontFolders); + var shaper = new TextShaper(font); + + //Text containing emoji + var inputText = "My long and 😝😱 bothersome 😝😱 text"; + var shapedText = (ShapedText)shaper.Shape(inputText); + var layout = new TextLayoutEngine(shaper); + + var text = layout.WrapText(inputText, 12, 20); + + + //// Act + //var lines = layout.WrapRichText(fragments, 1000); + + //var layout = new TextLayoutEngine(shaper); + } + [TestMethod] public void WrapRichText_SingleFragment_BehavesLikeSingleFont() { diff --git a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj index 76c52dc79..349f414af 100644 --- a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj +++ b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj @@ -95,4 +95,8 @@ + + + + diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 9c0d4abb1..2879be2d5 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -117,10 +117,10 @@ public string GetLineFragmentText(LineFragment rtFragment) } } - internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit) + internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit, int charsHandled) { //If we are splitting a fragment its position in the new line should be 0 - var newLineFragment = new LineFragment(origLf.FragmentIndex, 0); + var newLineFragment = new LineFragment(origLf.RtFragIdx, 0, charsHandled); newLineFragment.Width = origLf.Width - widthAtSplit; newLineFragment.SpaceWidth = origLf.SpaceWidth; diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index cdb8d7a2f..be500380d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -11,6 +11,8 @@ namespace EPPlus.Fonts.OpenType.Integration /// public class LineFragment { + public int StartIdxOriginal { get; internal set; } + /// /// Start char position within TextLineSimple.Text /// @@ -29,7 +31,10 @@ public class LineFragment /// public double SpaceWidth { get; internal set; } - internal LineFragment(int rtFragmentIdx, int idxWithinLine) + //public double AscentInPoints { get; private set; } + //public double DescentInPoints { get; private set; } + + internal LineFragment(int rtFragmentIdx, int idxWithinLine, int idxWithinTotal) { FragmentIndex = rtFragmentIdx; StartIdx = idxWithinLine; diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index d8d6eaf66..141b6fb11 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -152,6 +152,7 @@ private void ProcessFragment( StringBuilder lineBuilder, WrapStateRichText state) { + state.charsHandled = 0; var shaper = GetShaperForFont(fragment.Font); var options = fragment.Options ?? ShapingOptions.Default; int len = fragment.Text.Length; @@ -169,8 +170,8 @@ private void ProcessFragment( fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size); - - state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length) + state.charsHandled = 0; + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.charsHandled) { SpaceWidth = spaceWidth, StartIdx = lineBuilder.Length, @@ -211,6 +212,7 @@ private void ProcessFragment( WrapCurrentLine(lineBuilder, state, maxWidthPoints, charWidths[i]); } i++; + state.charsHandled++; } if (state.LineFrag.Width > 0) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 3bf3528ef..ba2fea526 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -7,6 +7,8 @@ namespace EPPlus.Fonts.OpenType.Integration internal class WrapStateRichText : WrapStateBase { internal LineFragment LineFrag = null; + internal int charsHandled = 0; + public WrapStateRichText(double lineWidth) { @@ -30,7 +32,7 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) { EndCurrentTextLine(); var spcWidthTemp = LineFrag.SpaceWidth; - LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment); + LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment, charsHandled); LineFrag.SpaceWidth = spcWidthTemp; } else @@ -96,7 +98,7 @@ internal void AdjustLineFragmentsForNextLine() var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart); + var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, charsHandled); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; _fragmentsForNextLine = new List(); From 9dffaaae9098800abf56d43f6e694d3ce3f31cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 30 Apr 2026 16:37:27 +0200 Subject: [PATCH 13/26] fixed merge conflict leftover --- .../Integration/DataHolders/TextLineSimple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 2879be2d5..cbd0e13f8 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -120,7 +120,7 @@ public string GetLineFragmentText(LineFragment rtFragment) internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit, int charsHandled) { //If we are splitting a fragment its position in the new line should be 0 - var newLineFragment = new LineFragment(origLf.RtFragIdx, 0, charsHandled); + var newLineFragment = new LineFragment(origLf.FragmentIndex, 0, charsHandled); newLineFragment.Width = origLf.Width - widthAtSplit; newLineFragment.SpaceWidth = origLf.SpaceWidth; From 377e34b0c8a0846597fcaf8044626d91da81e881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 30 Apr 2026 16:50:04 +0200 Subject: [PATCH 14/26] fixed some issues but it remains 0 --- .../Integration/DataHolders/TextLineCollection.cs | 3 ++- .../Integration/DataHolders/TextLineSimple.cs | 1 + .../Integration/LineFragmentOutput.cs | 8 +++++++- .../Integration/TextLayoutEngine.RichText.cs | 1 - 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index 95fce058d..dc3a473a1 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -171,7 +171,8 @@ internal void FinalizeTextLineData(List lines) data = new LineFragmentOutput( () => { return _originalFragments[idx]; }, () => { return lf.Width; }, - () => { return lf.StartIdx; }, + () => { return lf.StartIdx; }, + () => { return lf.StartIdxOriginal;}, lines[i].GetLineFragmentText(lf) ); } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index cbd0e13f8..4decc11bf 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -85,6 +85,7 @@ internal void FinalizeLineFragments(List originalFragments) () => { return originalFragments[lf.FragmentIndex]; }, () => { return lf.Width; }, () => { return lf.StartIdx; }, + () => { return lf.StartIdxOriginal; }, GetLineFragmentText(lf) ); LineFragments.Add(data); diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index 9b47269ea..88b1e3642 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -12,6 +12,10 @@ namespace EPPlus.Fonts.OpenType.Integration [DebuggerDisplay("{Text}")] public class LineFragmentOutput { + /// + /// Char idx within the whole input + /// + public int IdxWithinOriginal { get { return _getStartIdxOrig(); } } /// /// Char idx within the line /// @@ -43,13 +47,15 @@ public class LineFragmentOutput Func _getTextFragment; Func _getWidth; Func _getStartIdx; + Func _getStartIdxOrig; #endregion - internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx ,string text) + internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx, Func startOrig, string text) { _getTextFragment = getTextFragment; _getWidth = getWidth; _getStartIdx = startIdx; + _getStartIdxOrig = startOrig; Text = text; } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 141b6fb11..8289a07e6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -170,7 +170,6 @@ private void ProcessFragment( fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size); - state.charsHandled = 0; state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.charsHandled) { SpaceWidth = spaceWidth, From 3599e9beac9ff37b43cea81d3285f51c1c412bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 10:19:20 +0200 Subject: [PATCH 15/26] Bugged implementation of keeping track of rt and original indicies --- .../DataHolders/TextLineCollection.cs | 2 +- .../Integration/DataHolders/TextLineSimple.cs | 10 +++++----- .../Integration/LineFragment.cs | 14 ++++++++++++-- .../Integration/LineFragmentOutput.cs | 6 +++--- .../Integration/TextLayoutEngine.RichText.cs | 18 +++++++++--------- .../Integration/WrapStateRichText.cs | 10 ++++++---- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index dc3a473a1..fc3b24b37 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -172,7 +172,7 @@ internal void FinalizeTextLineData(List lines) () => { return _originalFragments[idx]; }, () => { return lf.Width; }, () => { return lf.StartIdx; }, - () => { return lf.StartIdxOriginal;}, + () => { return lf.StartRt;}, lines[i].GetLineFragmentText(lf) ); } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index 4decc11bf..e10263ee0 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -50,8 +50,6 @@ public class TextLineSimple /// public double GetWidthWithoutTrailingSpaces() { - lastFontSpaceWidth = InternalLineFragments.Last().SpaceWidth; - var trailingSpaceCount = 0; for (int i = Text.Count() - 1; i > 0; i--) @@ -79,13 +77,15 @@ public TextLineSimple() internal void FinalizeLineFragments(List originalFragments) { + lastFontSpaceWidth = InternalLineFragments.Last().SpaceWidth; + foreach (var lf in InternalLineFragments) { LineFragmentOutput data = new LineFragmentOutput( () => { return originalFragments[lf.FragmentIndex]; }, () => { return lf.Width; }, () => { return lf.StartIdx; }, - () => { return lf.StartIdxOriginal; }, + () => { return lf.StartRt; }, GetLineFragmentText(lf) ); LineFragments.Add(data); @@ -118,10 +118,10 @@ public string GetLineFragmentText(LineFragment rtFragment) } } - internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit, int charsHandled) + internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit, int charsRt, int charsTotal) { //If we are splitting a fragment its position in the new line should be 0 - var newLineFragment = new LineFragment(origLf.FragmentIndex, 0, charsHandled); + var newLineFragment = new LineFragment(origLf.FragmentIndex, 0, charsRt, charsTotal); newLineFragment.Width = origLf.Width - widthAtSplit; newLineFragment.SpaceWidth = origLf.SpaceWidth; diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index be500380d..6cf2495f2 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -11,7 +11,15 @@ namespace EPPlus.Fonts.OpenType.Integration /// public class LineFragment { - public int StartIdxOriginal { get; internal set; } + /// + /// Char idx within the TOTAL of the original input string regardless of what richtext/line + /// + public int StartOriginal { get; internal set; } + + /// + /// Start char position within input richtext fragment + /// + public int StartRt { get; internal set; } /// /// Start char position within TextLineSimple.Text @@ -34,10 +42,12 @@ public class LineFragment //public double AscentInPoints { get; private set; } //public double DescentInPoints { get; private set; } - internal LineFragment(int rtFragmentIdx, int idxWithinLine, int idxWithinTotal) + internal LineFragment(int rtFragmentIdx, int idxWithinLine, int startIdxRt, int idxOriginal) { FragmentIndex = rtFragmentIdx; StartIdx = idxWithinLine; + StartRt = startIdxRt; + StartOriginal = idxOriginal; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index 88b1e3642..f8b538005 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -15,7 +15,7 @@ public class LineFragmentOutput /// /// Char idx within the whole input /// - public int IdxWithinOriginal { get { return _getStartIdxOrig(); } } + public int StartRtIdx { get { return _getStartIdxRt(); } } /// /// Char idx within the line /// @@ -47,7 +47,7 @@ public class LineFragmentOutput Func _getTextFragment; Func _getWidth; Func _getStartIdx; - Func _getStartIdxOrig; + Func _getStartIdxRt; #endregion internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx, Func startOrig, string text) @@ -55,7 +55,7 @@ internal LineFragmentOutput(Func getTextFragment, Func get _getTextFragment = getTextFragment; _getWidth = getWidth; _getStartIdx = startIdx; - _getStartIdxOrig = startOrig; + _getStartIdxRt = startOrig; Text = text; } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 8289a07e6..cb18c96e6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -125,15 +125,18 @@ public List WrapRichTextLines( { double largestAscent = 0; double largestDescent = 0; + double largestFontSize = 0; foreach (var lineFragment in line.InternalLineFragments) { var frag = fragments[lineFragment.FragmentIndex]; if (frag == null) continue; largestAscent = Math.Max(frag.AscentPoints, largestAscent); largestDescent = Math.Max(frag.DescentPoints, largestDescent); + largestFontSize = Math.Max(largestFontSize, frag.Font.Size); } line.LargestAscent = largestAscent; line.LargestDescent = largestDescent; + line.LargestFontSize = largestFontSize; line.FinalizeLineFragments(fragments); } @@ -152,7 +155,7 @@ private void ProcessFragment( StringBuilder lineBuilder, WrapStateRichText state) { - state.charsHandled = 0; + state.CharIdxRt = 0; var shaper = GetShaperForFont(fragment.Font); var options = fragment.Options ?? ShapingOptions.Default; int len = fragment.Text.Length; @@ -170,16 +173,14 @@ private void ProcessFragment( fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size); - state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.charsHandled) - { - SpaceWidth = spaceWidth, - StartIdx = lineBuilder.Length, - FragmentIndex = state.CurrentFragmentIdx - }; + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.CharIdxRt, state.CharIdxWithinOriginal); + state.LineFrag.SpaceWidth = spaceWidth; int i = 0; while (i < len) { + state.CharIdxRt = i; + char c = fragment.Text[i]; if (IsLineBreak(c)) @@ -203,7 +204,6 @@ private void ProcessFragment( if (c == ' ') { state.SetAndLogWordStartState(lineBuilder.Length - 1); - state.SetAndLogWordStartState(lineBuilder.Length - 1); } if (state.CurrentLineWidth > maxWidthPoints) @@ -211,7 +211,7 @@ private void ProcessFragment( WrapCurrentLine(lineBuilder, state, maxWidthPoints, charWidths[i]); } i++; - state.charsHandled++; + state.CharIdxWithinOriginal++; } if (state.LineFrag.Width > 0) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index ba2fea526..2e82fb5fc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -7,8 +7,8 @@ namespace EPPlus.Fonts.OpenType.Integration internal class WrapStateRichText : WrapStateBase { internal LineFragment LineFrag = null; - internal int charsHandled = 0; - + internal int CharIdxRt = 0; + internal int CharIdxWithinOriginal = 0; public WrapStateRichText(double lineWidth) { @@ -32,7 +32,7 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) { EndCurrentTextLine(); var spcWidthTemp = LineFrag.SpaceWidth; - LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment, charsHandled); + LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment, CharIdxRt, CharIdxWithinOriginal); LineFrag.SpaceWidth = spcWidthTemp; } else @@ -98,7 +98,9 @@ internal void AdjustLineFragmentsForNextLine() var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, charsHandled); + var lenAtBreak = CharIdxWithinOriginal - origFragment.StartOriginal; + + var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, CharIdxRt, CharIdxWithinOriginal); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; _fragmentsForNextLine = new List(); From 3e3d95105052def5ae7293685b2b71d3769f46dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 14:26:04 +0200 Subject: [PATCH 16/26] Began implementing new Text Layout logic --- .../Integration/TextLayoutEngineTests.cs | 53 ++++- .../Integration/DataHolders/CharInfo.cs | 22 +- .../Integration/RichText/StyleRun.cs | 24 ++ .../Integration/RichText/SubParagraph.cs | 26 +++ .../Integration/RichText/TextParagraph.cs | 209 ++++++++++++++++++ .../Integration/RichText/TextSection.cs | 39 ++++ .../Integration/TextLayoutEngine.RichText.cs | 10 + .../Integration/WrapStateRichText.cs | 5 +- src/EPPlus/Drawing/Chart/ExcelManualLayout.cs | 2 +- 9 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs create mode 100644 src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 934fdf29b..49a2cd1df 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -610,7 +610,6 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() { List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16"}; - List comparatorLst = new() { "Strike", "Goudy size"}; var font2 = new MeasurementFont() { FontFamily = "Aptos Narrow", @@ -681,6 +680,58 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() Assert.AreEqual("16", smallestTextFragments[5]); } + [TestMethod] + public void EnsureRTCharIdxBecomesCorrectWhenBreaking() + { + List lstOfRichText = new() { "MyparticularilyLongWord", "WithAbsolutelyNoSpacesAtAllJustToBeDifficult" }; + var font = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + var font2 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new List() { font, font2 }; + + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); + var wrappedLines = layout.WrapRichTextLines(fragments, 225d); + + + List inputFragment = new List(); + List lstRtIdx = new List(); + List charAtPos = new List(); + List expectedStartRt = new List(); + + var totalString = "MyparticularilyLongWordWithAbsolutelyNoSpacesAtAllJustToBeDifficult"; + + + foreach (var line in wrappedLines) + { + foreach(LineFragment internalFragment in line.InternalLineFragments) + { + var fragIdx = internalFragment.FragmentIndex; + inputFragment.Add(fragIdx); + lstRtIdx.Add(internalFragment.StartRt); + charAtPos.Add(fragments[fragIdx].Text[internalFragment.StartRt]); + } + } + } + [TestMethod] public void WrapRichTextDifficultCaseCompare() { diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs index 599fa5e3c..298f3cb5f 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs @@ -7,17 +7,33 @@ namespace EPPlus.Fonts.OpenType.Integration //Leaving this for now. May be neccesary for vertical text internal class CharInfo { + /// + /// Index within AllText + /// internal int Index; + /// + /// The input Fragment Index + /// internal int Fragment; + /// + /// The input char index within the fragment + /// + internal int InnerIndex; + /// + /// Width in points of the char + /// + internal int Width; + + internal int Line; - internal int Width; + internal bool IsSeparator { get; set; } - public CharInfo(int index, int fragment, int line) + public CharInfo(int index, int fragment, int fragCharIdx/*, int line*/) { Index = index; Fragment = fragment; - Line = line; + //Line = line; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs new file mode 100644 index 000000000..b6aeb5d73 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration.RichText +{ + internal class StyleRun : TextSection + { + internal int FragmentIndex { get; private set; } + + internal StyleRun(int fragmentIndex, int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) + { + FragmentIndex = fragmentIndex; + } + + private double[] _charWidths; + + internal void SetCharWidths(double[] charWidths) + { + _charWidths = charWidths; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs new file mode 100644 index 000000000..bff42d1fe --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration.RichText +{ + internal class SubParagraph : TextSection + { + private List _styleRuns; + internal SubParagraph(int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) + { + } + + internal void AddStyleRun(StyleRun styleRun) + { + _styleRuns.Add(styleRun); + } + + //Return array so original list cannot be changed + internal StyleRun[] GetStyleRuns() + { + return _styleRuns.ToArray(); + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs new file mode 100644 index 000000000..ff35864ca --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace EPPlus.Fonts.OpenType.Integration.RichText +{ + /// + /// A list of rich-text fragments with relation to eachother is a paragraph + /// + internal class TextParagraph + { + /// + /// The Unalatered input fragments + /// + List InputFragments; + //The text of the entire paragraph + //regardless of linebreaking or style runs + string FullText; + List AllChars; + List SeparatorIndicies = new List(); + List ParagraphSeparatorIndicies = new List(); + List SubParagraphs = new List(); + List StyleRuns = new List(); + int FullTextLength = 0; + + TextLineCollection WrappedLineCollection; + + internal TextParagraph(List fragments, IEnumerable FontDirectories) + { + InputFragments = fragments; + //Extract basic info about the entire paragraph + InitalizeAllTextAndCharInfo(); + //Split into sub paragraphs + Segmentation(); + + //TODO: Bi-directional analysis (level-runs) will need to be merged with style runs + + //Segmenting Style Runs (Itimization) + Itemization(); + + //Apply shaping (Scripting and Cluster) in simplest of terms Measure widths/heights of characters in runs + Shaping(FontDirectories); + + ////Line-breaking + //Wrapping(FontDirectories, dou); + } + + /// + /// Extract basic info about the entire paragraph + /// + void InitalizeAllTextAndCharInfo() + { + List fragmentStartIdx = new List(); + int allCharIdx = 0; + int fragmentIdx = 0; + foreach (var fragment in InputFragments) + { + //var currentShaper = OpenTypeFonts.GetShaperForFont(fragment.Font); + + //var currShapedText = currentShaper.ShapeLight(fragment.Text); + + int spanIndex = 0; + fragmentIdx++; + foreach (var c in fragment.Text) + { + var currCharInfo = new CharInfo(allCharIdx, fragmentIdx, spanIndex); + AllChars.Add(currCharInfo); + if (char.IsSeparator(c)) + { + currCharInfo.IsSeparator = true; + SeparatorIndicies.Add(allCharIdx); + } + + spanIndex++; + allCharIdx++; + } + FullText += fragment.Text; + } + FullTextLength = allCharIdx; + } + + //Split paragraphs along paragraph separators + void Segmentation() + { + int lastParagraphIdx = 0; + UnicodeCategory category = UnicodeCategory.ParagraphSeparator; + foreach (var sepIdx in SeparatorIndicies) + { + if (CharUnicodeInfo.GetUnicodeCategory(FullText[sepIdx]) == category) + { + ParagraphSeparatorIndicies.Add(sepIdx); + var section = new SubParagraph(lastParagraphIdx, sepIdx, GetFullText, GetSection); + SubParagraphs.Add(section); + lastParagraphIdx = sepIdx; + } + } + + var lastSection = new SubParagraph(lastParagraphIdx, FullTextLength, GetFullText, GetSection); + SubParagraphs.Add(lastSection); + } + + /// + /// Seperating input into style-runs + /// + void Itemization() + { + var styleRunStartIdx = SubParagraphs[0].FullTextStart; + var currIdx = styleRunStartIdx; + var currFragIdx = AllChars[0].Fragment; + + for (int i = 0; i < SubParagraphs.Count; i++) + { + styleRunStartIdx = SubParagraphs[i].FullTextStart; + currIdx = styleRunStartIdx; + currFragIdx = AllChars[i].Fragment; + + for (int j = 0; j < SubParagraphs[i].Length; j++) + { + currIdx += j; + if (AllChars[currIdx].Fragment != currFragIdx) + { + //We have moved one beyond the last char to apply the given style. + //Therefore -1 (unless it is on the very first idx) + var styleRun = new StyleRun(currFragIdx, styleRunStartIdx, Math.Max(currIdx -1, 1), GetFullText, GetSection); + StyleRuns.Add(styleRun); + //TODO: Technically this should not get its own list it should refer back here + SubParagraphs[i].AddStyleRun(styleRun); + styleRunStartIdx = currIdx; + } + } + } + + var LastRun = new StyleRun(currFragIdx, styleRunStartIdx, Math.Max(currIdx - 1, 1), GetFullText, GetSection); + StyleRuns.Add(LastRun); + styleRunStartIdx = currIdx; + } + + /// + /// Shaping (calculating widths, heights etc.) + /// + /// + /// Set to false for slower more exact positioning (very rarely neccesary) + void Shaping(IEnumerable fontDirectories, bool shapeLight = true) + { + foreach (var styleRun in StyleRuns) + { + var inputFrag = InputFragments[styleRun.FragmentIndex]; + var shaper = OpenTypeFonts.GetShaperForFont(inputFrag.Font, fontDirectories); + if(shapeLight) + { + var shapedGlyphs = shaper.ShapeLight(styleRun.Text); + double[] charWidths = new double[styleRun.Length]; + shapedGlyphs.FillCharWidths(inputFrag.Font.Size, charWidths, styleRun.Length); + styleRun.SetCharWidths(charWidths); + } + else + { + throw new NotImplementedException("Proper shaping has not been implemented here yet"); + } + } + + var lastFragment = InputFragments[InputFragments.Count]; + var lastRun = StyleRuns[StyleRuns.Count]; + var lastShaper = OpenTypeFonts.GetShaperForFont(lastFragment.Font); + var lastShapedGlyphs = lastShaper.ShapeLight(StyleRuns[StyleRuns.Count].Text); + double[] lastCharWidths = new double[lastRun.Length]; + lastShapedGlyphs.FillCharWidths(lastFragment.Font.Size, lastCharWidths, lastRun.Length); + lastRun.SetCharWidths(lastCharWidths); + } + /// + /// Wrapping/line breaking + /// + /// + /// + /// + internal TextLineCollection Wrap(IEnumerable fontDirectories, double maxWidth) + { + var layoutEngine = OpenTypeFonts.GetTextLayoutEngineForFont(InputFragments[0].Font, fontDirectories); + var wrappedLines = layoutEngine.WrapRichTextLines(InputFragments, maxWidth); + WrappedLineCollection = new TextLineCollection(wrappedLines, InputFragments); + return WrappedLineCollection; + } + + string GetSection(int startIdx, int endIdx) + { + var subString = FullText.Substring(startIdx, endIdx - startIdx + 1); + return subString; + } + + string GetFullText() + { + return FullText; + } + + List GetCharInfoOfStyleRun(StyleRun run) + { + List infoLst = new List(); + for (int i = 0; i < run.Length; i++) + { + var charIdx = run.FullTextStart + i; + infoLst.Add(AllChars[charIdx]); + } + return infoLst; + } + + //Get paragraphindex + + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs new file mode 100644 index 000000000..9f83ba043 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace EPPlus.Fonts.OpenType.Integration.RichText +{ + internal class TextSection + { + int _startIdx; + int _endIdx; + + internal string FullText { get { return _getFullText(); } } + internal string Text { get { return _getText(_startIdx, _endIdx); } } + + Func _getFullText; + Func _getText; + + /// + /// Run starting char index in fulltext + /// + internal int FullTextStart { get { return _startIdx; } } + internal int Length { get { return _endIdx - _startIdx; } } + + /// + /// A section of text between start and end index + /// + /// A function that gets the fulltext + /// + /// A function that takes the substring between + /// start and end index on the fulltext + internal TextSection(int startIdx, int endIndex, Func getFullText, Func getText) + { + _startIdx = startIdx; + _endIdx = endIndex; + _getFullText = getFullText; + _getText = getText; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index cb18c96e6..79a15628e 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -13,6 +13,7 @@ Date Author Change 01/23/2025 EPPlus Software AB Fixed lastSpaceIndex bug in multi-fragment wrapping 02/23/2026 EPPlus Software AB Performance fix: Shape() → ShapeLight() in ProcessFragment *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Integration.RichText; using EPPlus.Fonts.OpenType.Utilities; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.Fonts; @@ -149,6 +150,15 @@ public List WrapRichTextLines( return state.Lines; } + private void ProcessStyleRun( + StyleRun run, + double maxWidthPoints, + StringBuilder lineBuilder, + WrapStateRichText state) + { + + } + private void ProcessFragment( TextFragment fragment, double maxWidthPoints, diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 2e82fb5fc..67b6bac97 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -49,6 +49,7 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) int _rtIdxAtWordStart = -1; int _listIdxWithinLine = -1; + int _totalCharsAtWordStart = -1; double _lineFragWidthAtWordStart = -1; internal void SetAndLogWordStartState(int wordStart) @@ -73,6 +74,8 @@ internal void SetAndLogWordStartState(int wordStart) //It will be the next index when added _listIdxWithinLine += 1; } + + _totalCharsAtWordStart = CharIdxWithinOriginal; } internal int GetFragIdxAtWordStart() @@ -98,7 +101,7 @@ internal void AdjustLineFragmentsForNextLine() var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - var lenAtBreak = CharIdxWithinOriginal - origFragment.StartOriginal; + var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, CharIdxRt, CharIdxWithinOriginal); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; diff --git a/src/EPPlus/Drawing/Chart/ExcelManualLayout.cs b/src/EPPlus/Drawing/Chart/ExcelManualLayout.cs index a74c3a4e5..7cf8fb27f 100644 --- a/src/EPPlus/Drawing/Chart/ExcelManualLayout.cs +++ b/src/EPPlus/Drawing/Chart/ExcelManualLayout.cs @@ -27,7 +27,7 @@ namespace OfficeOpenXml.Drawing.Chart /// For easiest use it is recommended to not change the modes of width or height. /// Left and Top are used to determine x and y position /// Width and Height to define the width and height of the element. - /// By default all elements originate from their default + /// By default all elements originate from their default position /// Use eLayoutMode.Edge to set origin to the edge of the chart for the relevant element. /// public class ExcelManualLayout : XmlHelper From 162f71daf319532ebf99c497f5259ae30e45a077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 15:02:27 +0200 Subject: [PATCH 17/26] First TextParagraph passing basic test --- .../Integration/TextLayoutEngineTests.cs | 38 ++++++++++++++ .../EPPlus.Fonts.OpenType.csproj | 1 - .../Integration/RichText/StyleRun.cs | 9 +++- .../Integration/RichText/SubParagraph.cs | 2 +- .../Integration/RichText/TextParagraph.cs | 50 +++++++++++------- .../Integration/TextLayoutEngine.RichText.cs | 51 +++++++++++++++++++ 6 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 49a2cd1df..04a82da3f 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -1,4 +1,5 @@ using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.Integration.RichText; using EPPlus.Fonts.OpenType.TextShaping; using EPPlus.Fonts.OpenType.Utils; using OfficeOpenXml.Interfaces.Drawing.Text; @@ -680,6 +681,42 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() Assert.AreEqual("16", smallestTextFragments[5]); } + [TestMethod] + public void TestParagraphs() + { + + List lstOfRichText = new() { "MyparticularilyLongWord", "WithAbsolutelyNoSpacesAtAllJustToBeDifficult" }; + var font = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + var font2 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new List() { font, font2 }; + + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + var paragraph = new TextParagraph(fragments, FontFolders); + + var styleRuns = paragraph.GetTextOfAllTextRuns(); + + Assert.AreEqual(lstOfRichText[0], styleRuns[0]); + Assert.AreEqual(lstOfRichText[1], styleRuns[1]); + } + [TestMethod] public void EnsureRTCharIdxBecomesCorrectWhenBreaking() { @@ -707,6 +744,7 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() fragments.Add(currentFrag); } + var paragraph = new TextParagraph(fragments, FontFolders); var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); diff --git a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj index 349f414af..81c4e33d6 100644 --- a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj +++ b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj @@ -96,7 +96,6 @@ - diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs index b6aeb5d73..79bd221e1 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs @@ -8,6 +8,7 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText internal class StyleRun : TextSection { internal int FragmentIndex { get; private set; } + internal double SpaceWidth { get; private set; } internal StyleRun(int fragmentIndex, int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) { @@ -16,9 +17,15 @@ internal StyleRun(int fragmentIndex, int startIdx, int endIndex, Func ge private double[] _charWidths; - internal void SetCharWidths(double[] charWidths) + internal void SetCharWidths(double[] charWidths, double spaceWidth) { _charWidths = charWidths; + SpaceWidth = spaceWidth; + } + + internal double GetCharWidthByIndex(int index) + { + return _charWidths[index]; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs index bff42d1fe..86e31df68 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs @@ -7,7 +7,7 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText { internal class SubParagraph : TextSection { - private List _styleRuns; + private List _styleRuns = new List(); internal SubParagraph(int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) { } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs index ff35864ca..678410e00 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs @@ -7,7 +7,7 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText /// /// A list of rich-text fragments with relation to eachother is a paragraph /// - internal class TextParagraph + public class TextParagraph { /// /// The Unalatered input fragments @@ -16,7 +16,7 @@ internal class TextParagraph //The text of the entire paragraph //regardless of linebreaking or style runs string FullText; - List AllChars; + List AllChars = new List(); List SeparatorIndicies = new List(); List ParagraphSeparatorIndicies = new List(); List SubParagraphs = new List(); @@ -25,7 +25,7 @@ internal class TextParagraph TextLineCollection WrappedLineCollection; - internal TextParagraph(List fragments, IEnumerable FontDirectories) + public TextParagraph(List fragments, IEnumerable FontDirectories) { InputFragments = fragments; //Extract basic info about the entire paragraph @@ -60,7 +60,6 @@ void InitalizeAllTextAndCharInfo() //var currShapedText = currentShaper.ShapeLight(fragment.Text); int spanIndex = 0; - fragmentIdx++; foreach (var c in fragment.Text) { var currCharInfo = new CharInfo(allCharIdx, fragmentIdx, spanIndex); @@ -75,6 +74,7 @@ void InitalizeAllTextAndCharInfo() allCharIdx++; } FullText += fragment.Text; + fragmentIdx++; } FullTextLength = allCharIdx; } @@ -104,35 +104,37 @@ void Segmentation() /// void Itemization() { - var styleRunStartIdx = SubParagraphs[0].FullTextStart; - var currIdx = styleRunStartIdx; + var subParagraphStartIdx = SubParagraphs[0].FullTextStart; + var currIdx = subParagraphStartIdx; var currFragIdx = AllChars[0].Fragment; + var lastRunIdx = 0; for (int i = 0; i < SubParagraphs.Count; i++) { - styleRunStartIdx = SubParagraphs[i].FullTextStart; - currIdx = styleRunStartIdx; + subParagraphStartIdx = SubParagraphs[i].FullTextStart; + currIdx = subParagraphStartIdx; currFragIdx = AllChars[i].Fragment; for (int j = 0; j < SubParagraphs[i].Length; j++) { - currIdx += j; + currIdx = subParagraphStartIdx + j; if (AllChars[currIdx].Fragment != currFragIdx) { //We have moved one beyond the last char to apply the given style. //Therefore -1 (unless it is on the very first idx) - var styleRun = new StyleRun(currFragIdx, styleRunStartIdx, Math.Max(currIdx -1, 1), GetFullText, GetSection); + var styleRun = new StyleRun(currFragIdx, lastRunIdx, Math.Max(currIdx -1, 1), GetFullText, GetSection); StyleRuns.Add(styleRun); //TODO: Technically this should not get its own list it should refer back here SubParagraphs[i].AddStyleRun(styleRun); - styleRunStartIdx = currIdx; + currFragIdx = AllChars[currIdx].Fragment; + lastRunIdx = currIdx; } } } - var LastRun = new StyleRun(currFragIdx, styleRunStartIdx, Math.Max(currIdx - 1, 1), GetFullText, GetSection); + var LastRun = new StyleRun(currFragIdx, lastRunIdx, Math.Max(currIdx, 1), GetFullText, GetSection); StyleRuns.Add(LastRun); - styleRunStartIdx = currIdx; + SubParagraphs[SubParagraphs.Count -1].AddStyleRun(LastRun); } /// @@ -151,7 +153,8 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) var shapedGlyphs = shaper.ShapeLight(styleRun.Text); double[] charWidths = new double[styleRun.Length]; shapedGlyphs.FillCharWidths(inputFrag.Font.Size, charWidths, styleRun.Length); - styleRun.SetCharWidths(charWidths); + var spaceWidth = shaper.Shape(" ").GetWidthInPoints(inputFrag.Font.Size); + styleRun.SetCharWidths(charWidths, spaceWidth); } else { @@ -159,13 +162,14 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) } } - var lastFragment = InputFragments[InputFragments.Count]; - var lastRun = StyleRuns[StyleRuns.Count]; + var lastFragment = InputFragments[InputFragments.Count-1]; + var lastRun = StyleRuns[StyleRuns.Count-1]; var lastShaper = OpenTypeFonts.GetShaperForFont(lastFragment.Font); - var lastShapedGlyphs = lastShaper.ShapeLight(StyleRuns[StyleRuns.Count].Text); + var lastShapedGlyphs = lastShaper.ShapeLight(lastRun.Text); double[] lastCharWidths = new double[lastRun.Length]; lastShapedGlyphs.FillCharWidths(lastFragment.Font.Size, lastCharWidths, lastRun.Length); - lastRun.SetCharWidths(lastCharWidths); + var LastspaceWidth = lastShaper.Shape(" ").GetWidthInPoints(lastFragment.Font.Size); + lastRun.SetCharWidths(lastCharWidths, LastspaceWidth); } /// /// Wrapping/line breaking @@ -192,6 +196,16 @@ string GetFullText() return FullText; } + public List GetTextOfAllTextRuns() + { + List runs = new List(); + foreach (var run in StyleRuns) + { + runs.Add(run.Text); + } + return runs; + } + List GetCharInfoOfStyleRun(StyleRun run) { List infoLst = new List(); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 79a15628e..d35dc7a1e 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -156,7 +156,58 @@ private void ProcessStyleRun( StringBuilder lineBuilder, WrapStateRichText state) { + state.CharIdxRt = 0; + state.CharIdxWithinOriginal = run.FullTextStart; + + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.CharIdxRt, state.CharIdxWithinOriginal); + state.LineFrag.SpaceWidth = run.SpaceWidth; + + int i = 0; + while (i < run.Length) + { + state.CharIdxRt = i; + + char c = run.Text[i]; + + if (IsLineBreak(c)) + { + HandleLineBreak(lineBuilder, state); + SkipLineBreakChars(run.Text, ref i); + + state.CurrentLineWidth = 0; + state.CurrentWordWidth = 0; + state.WordStart = -1; + state.LineStart = -1; + continue; + } + + var cWidth = run.GetCharWidthByIndex(i); + + state.CurrentLineWidth += cWidth; + state.CurrentWordWidth += cWidth; + state.LineFrag.Width += cWidth; + lineBuilder.Append(c); + + if (c == ' ') + { + state.SetAndLogWordStartState(lineBuilder.Length - 1); + } + + if (state.CurrentLineWidth > maxWidthPoints) + { + WrapCurrentLine(lineBuilder, state, maxWidthPoints, cWidth); + } + i++; + state.CharIdxWithinOriginal++; + } + + if (state.LineFrag.Width > 0) + { + state.CurrentTextLine.InternalLineFragments.Add(state.LineFrag); + } + + state.CurrentFragmentIdx++; } private void ProcessFragment( From f81165597e4cd231afb3ddb45955482332852842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 15:06:37 +0200 Subject: [PATCH 18/26] Sequence equal success --- .../Integration/TextLayoutEngineTests.cs | 9 ++++++++- .../Integration/RichText/TextParagraph.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 04a82da3f..a49bda152 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -710,11 +710,18 @@ public void TestParagraphs() } var paragraph = new TextParagraph(fragments, FontFolders); - var styleRuns = paragraph.GetTextOfAllTextRuns(); Assert.AreEqual(lstOfRichText[0], styleRuns[0]); Assert.AreEqual(lstOfRichText[1], styleRuns[1]); + + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); + var wrappedLines = layout.WrapRichTextLines(fragments, 225d); + + var wrappedLinesPara = paragraph.Wrap(FontFolders, 225d); + + wrappedLines.SequenceEqual(wrappedLinesPara); } [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs index 678410e00..c0cfd8ee3 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs @@ -177,7 +177,7 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) /// /// /// - internal TextLineCollection Wrap(IEnumerable fontDirectories, double maxWidth) + public TextLineCollection Wrap(IEnumerable fontDirectories, double maxWidth) { var layoutEngine = OpenTypeFonts.GetTextLayoutEngineForFont(InputFragments[0].Font, fontDirectories); var wrappedLines = layoutEngine.WrapRichTextLines(InputFragments, maxWidth); From 2d1e9c87e56656492244517ceea7c50eabf17422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 15:09:44 +0200 Subject: [PATCH 19/26] Renaming TextParagraph->layoutSystem, SubParagraph-> paragraph --- .../Integration/TextLayoutEngineTests.cs | 4 ++-- .../RichText/{TextParagraph.cs => LayoutSystem.cs} | 10 +++++----- .../RichText/{SubParagraph.cs => Paragraph.cs} | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/EPPlus.Fonts.OpenType/Integration/RichText/{TextParagraph.cs => LayoutSystem.cs} (95%) rename src/EPPlus.Fonts.OpenType/Integration/RichText/{SubParagraph.cs => Paragraph.cs} (71%) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index a49bda152..b49aabcb7 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -709,7 +709,7 @@ public void TestParagraphs() fragments.Add(currentFrag); } - var paragraph = new TextParagraph(fragments, FontFolders); + var paragraph = new LayoutSystem(fragments, FontFolders); var styleRuns = paragraph.GetTextOfAllTextRuns(); Assert.AreEqual(lstOfRichText[0], styleRuns[0]); @@ -751,7 +751,7 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() fragments.Add(currentFrag); } - var paragraph = new TextParagraph(fragments, FontFolders); + var paragraph = new LayoutSystem(fragments, FontFolders); var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs similarity index 95% rename from src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs rename to src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index c0cfd8ee3..c2b9200a4 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -7,7 +7,7 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText /// /// A list of rich-text fragments with relation to eachother is a paragraph /// - public class TextParagraph + public class LayoutSystem { /// /// The Unalatered input fragments @@ -19,13 +19,13 @@ public class TextParagraph List AllChars = new List(); List SeparatorIndicies = new List(); List ParagraphSeparatorIndicies = new List(); - List SubParagraphs = new List(); + List SubParagraphs = new List(); List StyleRuns = new List(); int FullTextLength = 0; TextLineCollection WrappedLineCollection; - public TextParagraph(List fragments, IEnumerable FontDirectories) + public LayoutSystem(List fragments, IEnumerable FontDirectories) { InputFragments = fragments; //Extract basic info about the entire paragraph @@ -89,13 +89,13 @@ void Segmentation() if (CharUnicodeInfo.GetUnicodeCategory(FullText[sepIdx]) == category) { ParagraphSeparatorIndicies.Add(sepIdx); - var section = new SubParagraph(lastParagraphIdx, sepIdx, GetFullText, GetSection); + var section = new Paragraph(lastParagraphIdx, sepIdx, GetFullText, GetSection); SubParagraphs.Add(section); lastParagraphIdx = sepIdx; } } - var lastSection = new SubParagraph(lastParagraphIdx, FullTextLength, GetFullText, GetSection); + var lastSection = new Paragraph(lastParagraphIdx, FullTextLength, GetFullText, GetSection); SubParagraphs.Add(lastSection); } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/Paragraph.cs similarity index 71% rename from src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs rename to src/EPPlus.Fonts.OpenType/Integration/RichText/Paragraph.cs index 86e31df68..a2697c2a6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/SubParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/Paragraph.cs @@ -5,10 +5,10 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText { - internal class SubParagraph : TextSection + internal class Paragraph : TextSection { private List _styleRuns = new List(); - internal SubParagraph(int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) + internal Paragraph(int startIdx, int endIndex, Func getFullText, Func getText) : base(startIdx, endIndex, getFullText, getText) { } From 860cd37bc65d82a27f470ed7eda784410dd18344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 15:18:51 +0200 Subject: [PATCH 20/26] Sequence equal when using runs all the way down --- .../Integration/RichText/LayoutSystem.cs | 25 ++++++++++++- .../Integration/RichText/StyleRun.cs | 2 +- .../Integration/RichText/TextSection.cs | 2 +- .../Integration/TextLayoutEngine.RichText.cs | 35 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index c2b9200a4..a046663cf 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -180,7 +180,30 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) public TextLineCollection Wrap(IEnumerable fontDirectories, double maxWidth) { var layoutEngine = OpenTypeFonts.GetTextLayoutEngineForFont(InputFragments[0].Font, fontDirectories); - var wrappedLines = layoutEngine.WrapRichTextLines(InputFragments, maxWidth); + var wrappedLines = layoutEngine.WrapRichTextRuns(StyleRuns, maxWidth); + + //var wrappedLines = layoutEngine.WrapRichTextLines(InputFragments, maxWidth); + //Calculate ascent and descent so later application can handle line-spacing + //This could be optimized by doing it during ProcessFragment but that is way bulkier/unclear + foreach (var line in wrappedLines) + { + double largestAscent = 0; + double largestDescent = 0; + double largestFontSize = 0; + foreach (var lineFragment in line.InternalLineFragments) + { + var frag = InputFragments[lineFragment.FragmentIndex]; + if (frag == null) continue; + largestAscent = Math.Max(frag.AscentPoints, largestAscent); + largestDescent = Math.Max(frag.DescentPoints, largestDescent); + largestFontSize = Math.Max(largestFontSize, frag.Font.Size); + } + line.LargestAscent = largestAscent; + line.LargestDescent = largestDescent; + line.LargestFontSize = largestFontSize; + + line.FinalizeLineFragments(InputFragments); + } WrappedLineCollection = new TextLineCollection(wrappedLines, InputFragments); return WrappedLineCollection; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs index 79bd221e1..1d94a0c1a 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs @@ -5,7 +5,7 @@ namespace EPPlus.Fonts.OpenType.Integration.RichText { - internal class StyleRun : TextSection + public class StyleRun : TextSection { internal int FragmentIndex { get; private set; } internal double SpaceWidth { get; private set; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs index 9f83ba043..5e6a151e0 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs @@ -4,7 +4,7 @@ using System.Text; namespace EPPlus.Fonts.OpenType.Integration.RichText { - internal class TextSection + public class TextSection { int _startIdx; int _endIdx; diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index d35dc7a1e..ea00953f7 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -150,6 +150,41 @@ public List WrapRichTextLines( return state.Lines; } + public List WrapRichTextRuns( + List fragments, + double maxWidthPoints) + { + if (fragments == null || fragments.Count == 0) + { + return new List(); + } + + _lineListBuffer.Clear(); + + var lineBuilder = new StringBuilder(512); + var state = new WrapStateRichText(0); + state.WordStart = -1; + state.LineStart = -1; + + foreach (var fragment in fragments) + { + if (string.IsNullOrEmpty(fragment.Text)) continue; + + ProcessStyleRun(fragment, maxWidthPoints, lineBuilder, state); + } + + FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart, state.CurrentTextLine); + state.CurrentTextLine.Width = state.CurrentLineWidth; + state.CurrentTextLine.Text = lineBuilder.ToString(); + state.EndCurrentTextLine(); + + if (_lineListBuffer.Count == 0) + { + _lineListBuffer.Add(string.Empty); + } + + return state.Lines; + } private void ProcessStyleRun( StyleRun run, double maxWidthPoints, From 240006b61b7b345ea6d9ff321236f70cf60a9d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 16:07:35 +0200 Subject: [PATCH 21/26] Fixed font-fallback issue --- .../Integration/RichText/LayoutSystem.cs | 10 +++++----- .../Integration/WrapStateRichText.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index a046663cf..f68ae4acc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -151,8 +151,8 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) if(shapeLight) { var shapedGlyphs = shaper.ShapeLight(styleRun.Text); - double[] charWidths = new double[styleRun.Length]; - shapedGlyphs.FillCharWidths(inputFrag.Font.Size, charWidths, styleRun.Length); + double[] charWidths = new double[styleRun.Length + 1]; + shapedGlyphs.FillCharWidths(inputFrag.Font.Size, charWidths, styleRun.Length + 1); var spaceWidth = shaper.Shape(" ").GetWidthInPoints(inputFrag.Font.Size); styleRun.SetCharWidths(charWidths, spaceWidth); } @@ -164,10 +164,10 @@ void Shaping(IEnumerable fontDirectories, bool shapeLight = true) var lastFragment = InputFragments[InputFragments.Count-1]; var lastRun = StyleRuns[StyleRuns.Count-1]; - var lastShaper = OpenTypeFonts.GetShaperForFont(lastFragment.Font); + var lastShaper = OpenTypeFonts.GetShaperForFont(lastFragment.Font, fontDirectories); var lastShapedGlyphs = lastShaper.ShapeLight(lastRun.Text); - double[] lastCharWidths = new double[lastRun.Length]; - lastShapedGlyphs.FillCharWidths(lastFragment.Font.Size, lastCharWidths, lastRun.Length); + double[] lastCharWidths = new double[lastRun.Length + 1]; + lastShapedGlyphs.FillCharWidths(lastFragment.Font.Size, lastCharWidths, lastRun.Length + 1); var LastspaceWidth = lastShaper.Shape(" ").GetWidthInPoints(lastFragment.Font.Size); lastRun.SetCharWidths(lastCharWidths, LastspaceWidth); } diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 67b6bac97..1ab94b766 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -101,7 +101,7 @@ internal void AdjustLineFragmentsForNextLine() var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; + //var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, CharIdxRt, CharIdxWithinOriginal); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; From 47b73a2da17f5632a7916477b809dc3c5503abdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 17:01:30 +0200 Subject: [PATCH 22/26] Ensure actual output is actually correct --- .../Integration/TextLayoutEngineTests.cs | 8 +++++++- .../Integration/DataHolders/TextLineCollection.cs | 5 +++-- .../Integration/DataHolders/TextLineSimple.cs | 1 + .../Integration/LineFragmentOutput.cs | 13 ++++++++++--- .../Integration/TextLayoutEngine.RichText.cs | 12 +++++++----- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index b49aabcb7..41957304a 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -721,7 +721,13 @@ public void TestParagraphs() var wrappedLinesPara = paragraph.Wrap(FontFolders, 225d); - wrappedLines.SequenceEqual(wrappedLinesPara); + Assert.AreEqual(wrappedLines.Count, wrappedLinesPara.Count); + + for (int i = 0; i < wrappedLines.Count; i++) + { + Assert.AreEqual(wrappedLines[i].Text, wrappedLinesPara[i].Text); + Assert.AreEqual(wrappedLines[i].Width, wrappedLinesPara[i].Width); + } } [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs index fc3b24b37..c61542c13 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -171,8 +171,9 @@ internal void FinalizeTextLineData(List lines) data = new LineFragmentOutput( () => { return _originalFragments[idx]; }, () => { return lf.Width; }, - () => { return lf.StartIdx; }, - () => { return lf.StartRt;}, + () => { return lf.StartIdx; }, + () => { return lf.StartRt; }, + () => { return lf.StartOriginal; }, lines[i].GetLineFragmentText(lf) ); } diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index e10263ee0..c975c1dcc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -86,6 +86,7 @@ internal void FinalizeLineFragments(List originalFragments) () => { return lf.Width; }, () => { return lf.StartIdx; }, () => { return lf.StartRt; }, + () => { return lf.StartOriginal; }, GetLineFragmentText(lf) ); LineFragments.Add(data); diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index f8b538005..6b8cc7794 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -13,7 +13,12 @@ namespace EPPlus.Fonts.OpenType.Integration public class LineFragmentOutput { /// - /// Char idx within the whole input + /// Char index within the FULL TEXT of all input fragments + /// + public int StartFullTextIdx { get { return _getFullTextIdx(); } } + + /// + /// Char idx within the input fragment /// public int StartRtIdx { get { return _getStartIdxRt(); } } /// @@ -48,14 +53,16 @@ public class LineFragmentOutput Func _getWidth; Func _getStartIdx; Func _getStartIdxRt; + Func _getFullTextIdx; #endregion - internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx, Func startOrig, string text) + internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx, Func startRt, Func fullTextIdx, string text) { _getTextFragment = getTextFragment; _getWidth = getWidth; _getStartIdx = startIdx; - _getStartIdxRt = startOrig; + _getStartIdxRt = startRt; + _getFullTextIdx = fullTextIdx; Text = text; } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index ea00953f7..113474a21 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -198,10 +198,8 @@ private void ProcessStyleRun( state.LineFrag.SpaceWidth = run.SpaceWidth; int i = 0; - while (i < run.Length) + while (i < (run.Length+1)) { - state.CharIdxRt = i; - char c = run.Text[i]; if (IsLineBreak(c)) @@ -216,6 +214,8 @@ private void ProcessStyleRun( continue; } + state.CharIdxRt = i; + var cWidth = run.GetCharWidthByIndex(i); state.CurrentLineWidth += cWidth; @@ -235,6 +235,7 @@ private void ProcessStyleRun( } i++; state.CharIdxWithinOriginal++; + state.CharIdxRt = i; } if (state.LineFrag.Width > 0) @@ -275,8 +276,6 @@ private void ProcessFragment( int i = 0; while (i < len) { - state.CharIdxRt = i; - char c = fragment.Text[i]; if (IsLineBreak(c)) @@ -291,6 +290,8 @@ private void ProcessFragment( continue; } + state.CharIdxRt = i; + state.CurrentLineWidth += charWidths[i]; state.CurrentWordWidth += charWidths[i]; state.LineFrag.Width += charWidths[i]; @@ -409,6 +410,7 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, //handle line data state.CurrentTextLine.Width = state.CurrentLineWidth - advanceWidth; state.CurrentTextLine.Text = line; + //state.CurrentTextLine. //Add the char that went over max to the next line state.CurrentLineWidth = 0; From e6e2c3b7974adc29cdde3d19e422a4f51f0e67be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 4 May 2026 17:09:56 +0200 Subject: [PATCH 23/26] Start of fixing overall index in SplitFragment --- .../Integration/WrapStateRichText.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 1ab94b766..e434a960b 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -50,6 +50,7 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) int _rtIdxAtWordStart = -1; int _listIdxWithinLine = -1; int _totalCharsAtWordStart = -1; + int _charIdxRtAtWordStart = -1; double _lineFragWidthAtWordStart = -1; internal void SetAndLogWordStartState(int wordStart) @@ -76,6 +77,7 @@ internal void SetAndLogWordStartState(int wordStart) } _totalCharsAtWordStart = CharIdxWithinOriginal; + _charIdxRtAtWordStart = CharIdxRt; } internal int GetFragIdxAtWordStart() @@ -98,10 +100,16 @@ internal void AdjustLineFragmentsForNextLine() //Do so before splitting CurrentTextLine.InternalLineFragments.Add(LineFrag); } + else + { + + } var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - //var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; + var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; + var endIndexOfOrigFragment = CharIdxWithinOriginal - CharIdxRt; + //var startIdxNewFragment = endIndexOfOrigFragment - var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, CharIdxRt, CharIdxWithinOriginal); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; From 8b174d0f5c0913383dccfaeedb3d90641a427b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 5 May 2026 09:47:13 +0200 Subject: [PATCH 24/26] Fixed tracking of indicies in both variants --- .../Integration/TextLayoutEngineTests.cs | 35 +++++++++++++++++++ .../Integration/LineFragmentOutput.cs | 5 +++ .../Integration/RichText/LayoutSystem.cs | 2 +- .../Integration/WrapStateRichText.cs | 18 ++++++---- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 41957304a..b43313131 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -730,6 +730,41 @@ public void TestParagraphs() } } + [TestMethod] + public void EnsureCorrectTotalIndex() + { + List lstOfRichText = new() { "aaaaaaaa aa aaaaaaaaaLa Strike", "Goudy size 16" }; + var font = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + var font2 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new List() { font, font2 }; + + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + var paragraph = new LayoutSystem(fragments, FontFolders); + var wrappedLines = paragraph.Wrap(FontFolders, 225d); + + Assert.AreEqual("StrikeGoudy size", wrappedLines[1].Text); + Assert.AreEqual(24, wrappedLines[1].LineFragments[0].StartFullTextIdx); + Assert.AreEqual(24, wrappedLines[1].LineFragments[0].StartRtIdx); + } + [TestMethod] public void EnsureRTCharIdxBecomesCorrectWhenBreaking() { diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs index 6b8cc7794..5afc69879 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -49,10 +49,15 @@ public class LineFragmentOutput /// Looks up the data in the internal class LineFragment instead of making copies /// #region Callbacks + [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func _getTextFragment; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func _getWidth; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func _getStartIdx; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func _getStartIdxRt; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func _getFullTextIdx; #endregion diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index f68ae4acc..4a4c727d5 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -33,7 +33,7 @@ public LayoutSystem(List fragments, IEnumerable FontDirect //Split into sub paragraphs Segmentation(); - //TODO: Bi-directional analysis (level-runs) will need to be merged with style runs + //TODO: Bi-directional analysis (level-runs) these will need to be merged with style runs //Segmenting Style Runs (Itimization) Itemization(); diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index e434a960b..819273f38 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -61,7 +61,12 @@ internal void SetAndLogWordStartState(int wordStart) _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; - if(CurrentTextLine.InternalLineFragments.Count == 0) + //Since we don't want the space itself to be the start pos but the first letter of the word. Use +1 + //TODO: Handle when no word after? Its never used if there isn't one so arguably we don't have to. + _totalCharsAtWordStart = CharIdxWithinOriginal + 1; + _charIdxRtAtWordStart = CharIdxRt + 1; + + if (CurrentTextLine.InternalLineFragments.Count == 0) { _listIdxWithinLine = 0; return; @@ -75,9 +80,6 @@ internal void SetAndLogWordStartState(int wordStart) //It will be the next index when added _listIdxWithinLine += 1; } - - _totalCharsAtWordStart = CharIdxWithinOriginal; - _charIdxRtAtWordStart = CharIdxRt; } internal int GetFragIdxAtWordStart() @@ -107,11 +109,13 @@ internal void AdjustLineFragmentsForNextLine() var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; - var lenAtBreak = CharIdxWithinOriginal - _totalCharsAtWordStart; - var endIndexOfOrigFragment = CharIdxWithinOriginal - CharIdxRt; + //var wordStartPos = _totalCharsAtWordStart; + //var wordBreakPos2 = CharIdxWithinOriginal; + //var endIndexOfOrigFragment = _charIdxRtAtWordStart; //var startIdxNewFragment = endIndexOfOrigFragment - + //var lnFragNewStartIdx = CharIdxWithinOriginal - _totalCharsAtWordStart; - var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, CharIdxRt, CharIdxWithinOriginal); + var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, _charIdxRtAtWordStart, _totalCharsAtWordStart); CurrentTextLine.InternalLineFragments[_listIdxWithinLine] = origFragment; _fragmentsForNextLine = new List(); From 21d73089d79927447170c4114d2123cc5edf33c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 5 May 2026 09:59:57 +0200 Subject: [PATCH 25/26] Functional tests --- .../Integration/TextLayoutEngineTests.cs | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index b43313131..09e8f75cd 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -797,25 +797,10 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); - - List inputFragment = new List(); - List lstRtIdx = new List(); - List charAtPos = new List(); - List expectedStartRt = new List(); - - var totalString = "MyparticularilyLongWordWithAbsolutelyNoSpacesAtAllJustToBeDifficult"; - - - foreach (var line in wrappedLines) - { - foreach(LineFragment internalFragment in line.InternalLineFragments) - { - var fragIdx = internalFragment.FragmentIndex; - inputFragment.Add(fragIdx); - lstRtIdx.Add(internalFragment.StartRt); - charAtPos.Add(fragments[fragIdx].Text[internalFragment.StartRt]); - } - } + Assert.AreEqual(5, wrappedLines[1].LineFragments[0].StartRtIdx); + Assert.AreEqual(16, wrappedLines[2].LineFragments[0].StartRtIdx); + Assert.AreEqual(28, wrappedLines[3].LineFragments[0].StartRtIdx); + Assert.AreEqual(40, wrappedLines[4].LineFragments[0].StartRtIdx); } [TestMethod] From 208d6c42e4225642009ec4fe5c46a1165c04546a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 5 May 2026 10:25:56 +0200 Subject: [PATCH 26/26] Added extra tests --- .../Integration/TextLayoutEngineTests.cs | 72 +++++++++++++++++++ .../Integration/RichText/LayoutSystem.cs | 5 ++ 2 files changed, 77 insertions(+) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 09e8f75cd..25c95ae54 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -730,6 +730,78 @@ public void TestParagraphs() } } + [TestMethod] + public void TestLayoutSystemParagraphChars() + { + List lstOfRichText = new() { "Here comes lorem ipsum\u2029 " + + "Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum[d] exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? [D]Quis autem vel eum i[r]ure reprehenderit, qui in ea voluptate velit esse, quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?\u2029 " + + "At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem reru[d]um facilis est e[r]t expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio, cumque nihil impedit, quo minus id, quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellend[a]us. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.\u2029 " + + "Let's see if we can recognize unicode paragraph separators" }; + var font = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + var fragments = new List() + { + new TextFragment() {Text = lstOfRichText[0], Font = font } + }; + + var layout = new LayoutSystem(fragments, FontFolders); + Assert.AreEqual(3, layout.GetParagraphSeparatorCount()); + } + + [TestMethod] + public void TestParagraphs_DifficultCase() + { + List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16" }; + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + List fonts = new() { font2, font3, font4, font5 }; + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + + var paragraph = new LayoutSystem(fragments, FontFolders); + var wrappedLines = paragraph.Wrap(FontFolders, 225d); + + var line1 = wrappedLines[0]; + } + [TestMethod] public void EnsureCorrectTotalIndex() { diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index 4a4c727d5..278cd0ccd 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -229,6 +229,11 @@ public List GetTextOfAllTextRuns() return runs; } + public int GetParagraphSeparatorCount() + { + return ParagraphSeparatorIndicies.Count; + } + List GetCharInfoOfStyleRun(StyleRun run) { List infoLst = new List();