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 be029a5db..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; } @@ -345,7 +351,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) { @@ -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; } @@ -460,7 +468,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 +476,8 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth); + var idx = _newTextFragments.IndexOf(lineFragment.OriginalTextFragment); + AddRenderItemTextRun(p.TextRuns[idx], displayText, prevWidth); } TextRunItem runItem = Runs.Last(); 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 new file mode 100644 index 000000000..5472bea66 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/DataHolders/TextLineSimpleTests.cs @@ -0,0 +1,117 @@ +using EPPlus.Fonts.OpenType.Integration; +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; +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 wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); + + + } + + [TestMethod] + public void TestLineFragmentSeeWhatLinesUseWhatRichText() + { + 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 wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + var wrappedCollection = layout.WrapRichTextLineCollection(fragments, maxSizePoints); + + var lines = wrappedCollection.GetTextLinesThatUse(fragments[4]); + var specificFragments = wrappedCollection.GetLineFragmentsThatUse(fragments[4]); + var lineIndicies = wrappedCollection.GetLineNumbersThatUse(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); + + var expectedArr = new int[] { 3, 4 }; + expectedArr.SequenceCompareTo(lineIndicies); + } + + 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.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 7f91bcd57..25c95ae54 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; @@ -212,6 +213,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() { @@ -524,8 +551,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 +601,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); } @@ -584,7 +611,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", @@ -631,7 +657,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 +665,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); @@ -655,6 +681,200 @@ 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 LayoutSystem(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); + + 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] + 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() + { + 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() + { + 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 LayoutSystem(fragments, FontFolders); + + var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); + var wrappedLines = layout.WrapRichTextLines(fragments, 225d); + + 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] public void WrapRichTextDifficultCaseCompare() { @@ -736,25 +956,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 +1203,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/EPPlus.Fonts.OpenType.csproj b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj index 76c52dc79..81c4e33d6 100644 --- a/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj +++ b/src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj @@ -95,4 +95,7 @@ + + + diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs index c6ddae562..298f3cb5f 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/CharInfo.cs @@ -4,19 +4,36 @@ using System.Text; 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/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 new file mode 100644 index 000000000..2206e1add --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/IRichTextInfoBase.cs @@ -0,0 +1,41 @@ +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 +{ + /// + /// Interface for pdf/svg/future richtext users to unify richtext styling + /// + 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/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/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/TextFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollection.cs deleted file mode 100644 index cdd23b3bc..000000000 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextFragmentCollection.cs +++ /dev/null @@ -1,388 +0,0 @@ -//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; -// } -// } -//} 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..c61542c13 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineCollection.cs @@ -0,0 +1,207 @@ +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + + + +namespace EPPlus.Fonts.OpenType.Integration +{ + + public class TextLineCollection : List, IEnumerable + { + /// + /// 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 + /// + /// + /// + /// + public List GetTextLinesThatUse(TextFragment fragment) + { + var idx = _originalFragments.IndexOf(fragment); + + if(idx != -1) + { + List retLines = new List(); + foreach (var key in fragIdLookup[idx].Keys) + { + retLines.Add(this[key]); + } + return retLines; + } + else + { + return null; + } + } + /// + /// Returns null if fragment is not found in any linefragments + /// + /// + /// + public List GetInternalLineFragmentsThatUse(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].InternalLineFragments[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 + /// 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; + } + + /// + /// If using this MUST call FinalizeTextLineData to finish the information gathering + /// + /// + 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].InternalLineFragments) + { + 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; }, + () => { return lf.StartRt; }, + () => { return lf.StartOriginal; }, + lines[i].GetLineFragmentText(lf) + ); + } + else + { + //If already calculate don't duplicate just use the data + data = lines[i].LineFragments[fragCount]; + } + + LineFragments.Add(data); + + fragCount++; + } + Add(lines[i]); + } + } + + public TextLineCollection(List lines, List originalFragments) + { + _originalFragments = originalFragments; + + for(int i = 0; i < originalFragments.Count; i++) + { + fragIdLookup.Add(i, new Dictionary>()); + } + + FinalizeTextLineData(lines); + } + } +} + diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index e98fe3b46..c975c1dcc 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -1,14 +1,25 @@ 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 { + [DebuggerDisplay("{Text}")] public class TextLineSimple { - public List LineFragments { get; internal set; } = new List(); + /// + /// 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; } /// @@ -39,13 +50,11 @@ public class TextLineSimple /// public double GetWidthWithoutTrailingSpaces() { - lastFontSpaceWidth = LineFragments.Last().SpaceWidth; - 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 +62,7 @@ public double GetWidthWithoutTrailingSpaces() trailingSpaceCount++; } - if(WasWrappedOnSpace) + if (WasWrappedOnSpace) { trailingSpaceCount++; } @@ -66,17 +75,27 @@ public TextLineSimple() { } - public TextLineSimple(string text, double largestFontSize, double largestAscent, double largestDescent) + internal void FinalizeLineFragments(List originalFragments) { - Text = text; - LargestFontSize = largestFontSize; - LargestAscent = largestAscent; - LargestDescent = largestDescent; + 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.StartRt; }, + () => { return lf.StartOriginal; }, + GetLineFragmentText(lf) + ); + LineFragments.Add(data); + } } 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}"); } @@ -88,24 +107,26 @@ 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); } } - internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit) + 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.RtFragIdx, 0); + var newLineFragment = new LineFragment(origLf.FragmentIndex, 0, charsRt, charsTotal); newLineFragment.Width = origLf.Width - widthAtSplit; + newLineFragment.SpaceWidth = origLf.SpaceWidth; + origLf.Width = widthAtSplit; return newLineFragment; 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/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 26dbc439b..6cf2495f2 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; @@ -10,21 +11,43 @@ namespace EPPlus.Fonts.OpenType.Integration /// public class LineFragment { + /// + /// 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 + /// public int StartIdx { get; set; } + /// + /// Width of this fragment + /// public double Width { get; set; } - public int RtFragIdx { get; set; } + /// + /// Index of original TextFragment + /// + public int FragmentIndex { get; set; } + /// + /// Width of a space in the original TextFragment + /// public double SpaceWidth { get; internal set; } //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, int startIdxRt, int idxOriginal) { - RtFragIdx = rtFragmentIdx; + FragmentIndex = rtFragmentIdx; StartIdx = idxWithinLine; - //AscentInPoints = ascentInPoints; - //DescentInPoints = descentInPoints; + StartRt = startIdxRt; + StartOriginal = idxOriginal; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs new file mode 100644 index 000000000..5afc69879 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragmentOutput.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration +{ + /// + /// Finalized output which uses callbacks to get data but can never have data set. + /// + [DebuggerDisplay("{Text}")] + public class LineFragmentOutput + { + /// + /// 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(); } } + /// + /// Char idx within the line + /// + public int StartIdx { get { return _getStartIdx(); } } + /// + /// Width of this fragment + /// + public double Width { get { return _getWidth(); } } + + /// + /// 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 + [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 + + internal LineFragmentOutput(Func getTextFragment, Func getWidth, Func startIdx, Func startRt, Func fullTextIdx, string text) + { + _getTextFragment = getTextFragment; + _getWidth = getWidth; + _getStartIdx = startIdx; + _getStartIdxRt = startRt; + _getFullTextIdx = fullTextIdx; + Text = text; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs new file mode 100644 index 000000000..278cd0ccd --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -0,0 +1,251 @@ +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 + /// + public class LayoutSystem + { + /// + /// The Unalatered input fragments + /// + List InputFragments; + //The text of the entire paragraph + //regardless of linebreaking or style runs + string FullText; + List AllChars = new List(); + List SeparatorIndicies = new List(); + List ParagraphSeparatorIndicies = new List(); + List SubParagraphs = new List(); + List StyleRuns = new List(); + int FullTextLength = 0; + + TextLineCollection WrappedLineCollection; + + public LayoutSystem(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) these 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; + 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; + fragmentIdx++; + } + 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 Paragraph(lastParagraphIdx, sepIdx, GetFullText, GetSection); + SubParagraphs.Add(section); + lastParagraphIdx = sepIdx; + } + } + + var lastSection = new Paragraph(lastParagraphIdx, FullTextLength, GetFullText, GetSection); + SubParagraphs.Add(lastSection); + } + + /// + /// Seperating input into style-runs + /// + void Itemization() + { + var subParagraphStartIdx = SubParagraphs[0].FullTextStart; + var currIdx = subParagraphStartIdx; + var currFragIdx = AllChars[0].Fragment; + var lastRunIdx = 0; + + for (int i = 0; i < SubParagraphs.Count; i++) + { + subParagraphStartIdx = SubParagraphs[i].FullTextStart; + currIdx = subParagraphStartIdx; + currFragIdx = AllChars[i].Fragment; + + for (int j = 0; j < SubParagraphs[i].Length; 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, 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); + currFragIdx = AllChars[currIdx].Fragment; + lastRunIdx = currIdx; + } + } + } + + var LastRun = new StyleRun(currFragIdx, lastRunIdx, Math.Max(currIdx, 1), GetFullText, GetSection); + StyleRuns.Add(LastRun); + SubParagraphs[SubParagraphs.Count -1].AddStyleRun(LastRun); + } + + /// + /// 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 + 1]; + shapedGlyphs.FillCharWidths(inputFrag.Font.Size, charWidths, styleRun.Length + 1); + var spaceWidth = shaper.Shape(" ").GetWidthInPoints(inputFrag.Font.Size); + styleRun.SetCharWidths(charWidths, spaceWidth); + } + else + { + throw new NotImplementedException("Proper shaping has not been implemented here yet"); + } + } + + var lastFragment = InputFragments[InputFragments.Count-1]; + var lastRun = StyleRuns[StyleRuns.Count-1]; + var lastShaper = OpenTypeFonts.GetShaperForFont(lastFragment.Font, fontDirectories); + var lastShapedGlyphs = lastShaper.ShapeLight(lastRun.Text); + 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); + } + /// + /// Wrapping/line breaking + /// + /// + /// + /// + public TextLineCollection Wrap(IEnumerable fontDirectories, double maxWidth) + { + var layoutEngine = OpenTypeFonts.GetTextLayoutEngineForFont(InputFragments[0].Font, fontDirectories); + 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; + } + + string GetSection(int startIdx, int endIdx) + { + var subString = FullText.Substring(startIdx, endIdx - startIdx + 1); + return subString; + } + + string GetFullText() + { + return FullText; + } + + public List GetTextOfAllTextRuns() + { + List runs = new List(); + foreach (var run in StyleRuns) + { + runs.Add(run.Text); + } + return runs; + } + + public int GetParagraphSeparatorCount() + { + return ParagraphSeparatorIndicies.Count; + } + + 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/Paragraph.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/Paragraph.cs new file mode 100644 index 000000000..a2697c2a6 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/Paragraph.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 Paragraph : TextSection + { + private List _styleRuns = new List(); + internal Paragraph(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/StyleRun.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs new file mode 100644 index 000000000..1d94a0c1a --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/StyleRun.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration.RichText +{ + public 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) + { + FragmentIndex = fragmentIndex; + } + + private 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/TextSection.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/TextSection.cs new file mode 100644 index 000000000..5e6a151e0 --- /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 +{ + public 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/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs index bf7bb0e05..aef9ae09b 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,45 @@ 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; } } + + /// + /// Simple class to provide some kind of fallback/defaults + /// + 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 e563926c1..113474a21 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; @@ -82,6 +83,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) @@ -116,15 +126,20 @@ public List WrapRichTextLines( { double largestAscent = 0; double largestDescent = 0; - foreach (var lineFragment in line.LineFragments) + double largestFontSize = 0; + foreach (var lineFragment in line.InternalLineFragments) { - 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); + largestFontSize = Math.Max(largestFontSize, frag.Font.Size); } line.LargestAscent = largestAscent; line.LargestDescent = largestDescent; + line.LargestFontSize = largestFontSize; + + line.FinalizeLineFragments(fragments); } if (_lineListBuffer.Count == 0) @@ -135,12 +150,109 @@ 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, + 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+1)) + { + 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; + } + + state.CharIdxRt = i; + + 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++; + state.CharIdxRt = i; + } + + if (state.LineFrag.Width > 0) + { + state.CurrentTextLine.InternalLineFragments.Add(state.LineFrag); + } + + state.CurrentFragmentIdx++; + } + private void ProcessFragment( TextFragment fragment, double maxWidthPoints, StringBuilder lineBuilder, WrapStateRichText state) { + state.CharIdxRt = 0; var shaper = GetShaperForFont(fragment.Font); var options = fragment.Options ?? ShapingOptions.Default; int len = fragment.Text.Length; @@ -158,11 +270,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.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.CharIdxRt, state.CharIdxWithinOriginal); state.LineFrag.SpaceWidth = spaceWidth; - state.LineFrag.StartIdx = lineBuilder.Length; - state.LineFrag.RtFragIdx = state.CurrentFragmentIdx; int i = 0; while (i < len) @@ -181,6 +290,8 @@ private void ProcessFragment( continue; } + state.CharIdxRt = i; + state.CurrentLineWidth += charWidths[i]; state.CurrentWordWidth += charWidths[i]; state.LineFrag.Width += charWidths[i]; @@ -190,7 +301,6 @@ private void ProcessFragment( if (c == ' ') { state.SetAndLogWordStartState(lineBuilder.Length - 1); - state.SetAndLogWordStartState(lineBuilder.Length - 1); } if (state.CurrentLineWidth > maxWidthPoints) @@ -198,11 +308,12 @@ private void ProcessFragment( WrapCurrentLine(lineBuilder, state, maxWidthPoints, charWidths[i]); } i++; + state.CharIdxWithinOriginal++; } if (state.LineFrag.Width > 0) { - state.CurrentTextLine.LineFragments.Add(state.LineFrag); + state.CurrentTextLine.InternalLineFragments.Add(state.LineFrag); } state.CurrentFragmentIdx++; @@ -299,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; 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/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index d645430ca..819273f38 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 CharIdxRt = 0; + internal int CharIdxWithinOriginal = 0; public WrapStateRichText(double lineWidth) { @@ -15,9 +17,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); } @@ -30,13 +32,13 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) { EndCurrentTextLine(); var spcWidthTemp = LineFrag.SpaceWidth; - LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment); + LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment, CharIdxRt, CharIdxWithinOriginal); LineFrag.SpaceWidth = spcWidthTemp; } else { //_fragmentsForNextLine.Add(LineFrag); - nextLine.LineFragments = _fragmentsForNextLine; + nextLine.InternalLineFragments = _fragmentsForNextLine; //LineFrag.StartIdx = ; Lines.Add(CurrentTextLine); @@ -47,6 +49,8 @@ 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) @@ -57,15 +61,20 @@ internal void SetAndLogWordStartState(int wordStart) _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; - if(CurrentTextLine.LineFragments.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; } - _listIdxWithinLine = CurrentTextLine.LineFragments.Count - 1; + _listIdxWithinLine = CurrentTextLine.InternalLineFragments.Count - 1; - if (CurrentTextLine.LineFragments[_listIdxWithinLine].RtFragIdx < _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,29 +100,48 @@ 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); + } + else + { + } - var origFragment = CurrentTextLine.LineFragments[_listIdxWithinLine]; + var origFragment = CurrentTextLine.InternalLineFragments[_listIdxWithinLine]; + + //var wordStartPos = _totalCharsAtWordStart; + //var wordBreakPos2 = CharIdxWithinOriginal; + //var endIndexOfOrigFragment = _charIdxRtAtWordStart; + //var startIdxNewFragment = endIndexOfOrigFragment - + //var lnFragNewStartIdx = CharIdxWithinOriginal - _totalCharsAtWordStart; - var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart); - CurrentTextLine.LineFragments[_listIdxWithinLine] = origFragment; + var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart, _charIdxRtAtWordStart, _totalCharsAtWordStart); + 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--) { + //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.LineFragments[i]); + _fragmentsForNextLine.Insert(0, CurrentTextLine.InternalLineFragments[i]); //Remove it from the old - CurrentTextLine.LineFragments.RemoveAt(i); + CurrentTextLine.InternalLineFragments.RemoveAt(i); } 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) { 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; } - } -} 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