From a8fe9093a1d0d0b4639d5c412e959cbb796228f9 Mon Sep 17 00:00:00 2001 From: PDFsharp-Team Date: Wed, 11 Oct 2023 10:31:01 +0200 Subject: [PATCH] =?UTF-8?q?=E2=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- dev/run-tests.ps1 | 4 + docs/DevNotes.md | 12 +- .../Capabilities.cs | 4 +- .../DocumentObjectModel.IO/DdlScanner.cs | 9 +- .../LogMessages.cs | 1 + .../DocumentObjectModel.Internals/Logging.cs | 6 +- .../DocumentObjectModel.Shapes.Charts/Axis.cs | 2 +- .../AxisTitle.cs | 2 +- .../DocumentObjectModel.Shapes/Barcode.cs | 6 +- .../DocumentObjectModel.Shapes/WrapFormat.cs | 2 +- .../DocumentObjectModel.Tables/Cell.cs | 4 +- .../DocumentObjectModel.Tables/Table.cs | 2 +- .../ElementsExtensions.cs | 6 +- .../MergedCellList.cs | 162 +++++++++++++---- .../DocumentObjectModel/Borders.cs | 107 +++++------ .../DocumentObjectModel/Document.cs | 22 +++ .../DocumentObjectModel/Font.cs | 4 +- .../DocumentObjectModel/FormattedText.cs | 2 +- .../DocumentObjectModel/Paragraph.cs | 3 +- .../DocumentObjectModel/ParagraphElements.cs | 19 +- .../DocumentObjectModel/Style.cs | 8 +- .../Rendering/ParagraphRenderer.cs | 34 ++-- .../Rendering/TableRenderer.cs | 14 +- .../Rendering/enums/ElementAlignment.cs | 10 +- .../Rendering/enums/Floating.cs | 14 +- .../Rendering/enums/HorizontalReference.cs | 6 +- .../Rendering/enums/ImageFailure.cs | 10 +- .../Rendering/enums/VerticalReference.cs | 10 +- .../RtfRendering/CellFormatRenderer.cs | 2 +- .../RtfRendering/RtfWriter.cs | 2 +- .../ParagraphTests.cs | 166 +++++++++++++++++- .../TableTests.cs | 135 +++++++++++++- .../MigraDoc.Tests-gdi.csproj | 1 + .../MigraDoc.Tests-wpf.csproj | 1 + .../tests/MigraDoc.Tests/RtfRendererTests.cs | 116 ++++++++++-- .../tests/MigraDoc.Tests/TextTests.cs | 106 +++++++++++ .../src/PdfSharp.Charting/Charting/Point.cs | 2 +- .../Drawing.Internal/ImageImporterJpeg.cs | 45 ++++- .../PdfSharp/Drawing.Layout/XTextFormatter.cs | 2 +- .../PdfSharp/Drawing.Pdf/PdfGraphicsState.cs | 2 +- .../Drawing.Pdf/XGraphicsPdfRenderer.cs | 19 +- .../src/PdfSharp/Drawing/FontHelper.cs | 11 +- .../PDFsharp/src/PdfSharp/Drawing/XFont.cs | 2 +- .../src/PdfSharp/Drawing/XGraphics.cs | 11 +- .../PdfSharp/Drawing/XGraphicsContainer.cs | 4 +- .../PDFsharp/src/PdfSharp/Drawing/XImage.cs | 2 +- .../PdfSharp/Fonts.OpenType/FontDescriptor.cs | 2 +- .../PdfSharp/Fonts.OpenType/GlyphDataTable.cs | 14 +- .../Fonts.OpenType/OpenTypeDescriptor.cs | 60 +++++-- .../Fonts.OpenType/OpenTypeFontTables.cs | 113 ++++++++++-- .../Fonts.OpenType/OpenTypeFontface.cs | 6 +- .../PDFsharp/src/PdfSharp/Fonts/CMapInfo.cs | 39 +++- .../Pdf.Advanced/PdfImage.FaxEncode.cs | 2 +- .../src/PdfSharp/Pdf.Advanced/PdfImage.cs | 12 +- .../src/PdfSharp/Pdf.Advanced/PdfReference.cs | 2 +- .../src/PdfSharp/Pdf.Advanced/PdfResources.cs | 2 +- .../PdfSharp/Pdf.Advanced/PdfToUnicodeMap.cs | 2 +- .../src/PdfSharp/Pdf.Advanced/PdfType0Font.cs | 2 +- .../src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs | 2 +- .../PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs | 6 +- .../src/PdfSharp/Pdf.IO/ShiftStack.cs | 2 +- .../PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs | 2 +- .../PDFsharp/src/PdfSharp/Pdf/PdfInteger.cs | 2 +- .../PDFsharp/src/PdfSharp/Pdf/PdfObject.cs | 4 +- .../PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs | 2 +- .../tests/PdfSharp.Tests/ImageTests.cs | 4 +- .../tests/PdfSharp.Tests/TextTests.cs | 54 ++++++ .../shared/src/PdfSharp.Quality/Feature.cs | 2 +- .../Font/fontresolving/NewFontResolver.cs | 2 + .../Font/fontresolving/SegoeUIFontResolver.cs | 2 +- .../src/PDFsharp/src/HelloWorld/Program.cs | 8 - 72 files changed, 1159 insertions(+), 307 deletions(-) create mode 100644 src/foundation/src/MigraDoc/tests/MigraDoc.Tests/TextTests.cs create mode 100644 src/foundation/src/PDFsharp/tests/PdfSharp.Tests/TextTests.cs diff --git a/README.md b/README.md index a3e975dd..13d26b10 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PDFsharp & MigraDoc 6.0 -Version **6.0.0-preview-3** -Published **2023-07-27** +Version **6.0.0-preview-4** +Published **2023-10-11** This is the third preview of the **PDFsharp** project, the main project of PDFsharp & MigraDoc 6.0 with updates for C# 10 and .NET 6.0. diff --git a/dev/run-tests.ps1 b/dev/run-tests.ps1 index c3716780..860ded03 100644 --- a/dev/run-tests.ps1 +++ b/dev/run-tests.ps1 @@ -39,6 +39,10 @@ function GetDllInfo($project) { $debugFolder = Join-Path -Path $projectFolder "bin\debug" $dllFile = Get-ChildItem -Path $debugFolder -Filter "$projectName.dll" -Recurse -ErrorAction SilentlyContinue -Force | Select-Object -first 1 + + if ($dllFile -eq $null) { + Write-Error "Could not finde file `"$debugFolder\**\$projectName.dll`". Maybe Debug Build has not been built." + } $dllFolder = $dllFile.Directory.FullName | Resolve-Path -Relative $dllFileName = $dllFile.Name diff --git a/docs/DevNotes.md b/docs/DevNotes.md index 2c8dcee5..faaf8577 100644 --- a/docs/DevNotes.md +++ b/docs/DevNotes.md @@ -6,19 +6,18 @@ README.md - ## Comments ### DELETE yyyy-mm-dd -Here is code that is replaced by newer code and should be deleted in the future. -But keeps here at the moment as reference in case the new code has bugs. +Here is code that was replaced by newer code and should be deleted in the future. +But kept here at the moment as reference in case the new code has bugs. After the specified date the code can be deleted. ### KEEP -Here is older code that is not used anymore but keeps here for +Here is older code that is not used anymore but kept here for documentation or reference purposes and shall not be removed. ### TODO @@ -36,9 +35,8 @@ for better reliability. ### OBSERVATION - ### EXPERIMENTAL - ### TEST -Here is code that should be coved by (more) unit tests. \ No newline at end of file + +Here is code that should be coved by (more) unit tests. diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Capabilities.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Capabilities.cs index 2d226aa6..a998fa7c 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Capabilities.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/Capabilities.cs @@ -76,7 +76,9 @@ public static class BackwardCompatibility public static bool DoNotCreateLastTable { get; set; } = false; /// - /// NYI + /// Gets or sets a flag that defines what LastSection does if no section exists. + /// If false, which is the default value, a new section is created and returned. + /// If true, no section will be created and null is returned instead. /// public static bool DoNotCreateLastSection { get; set; } = false; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs index 86aecd09..298fc54f 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.IO/DdlScanner.cs @@ -607,8 +607,7 @@ internal bool MoveToNextParagraphContentLine(bool rootLevel) return false; } - //TODO NiSc - //NYI + //TODO NiSc NYI //Check.NotImplemented("empty line at non-root level"); } break; @@ -743,7 +742,7 @@ internal int GetTokenValueAsInt() string number = _token.Substring(2); return Int32.Parse(number, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); } - //TODO NiSc + //TODO NiSc Check? //Check.Assert(false); return 0; } @@ -762,7 +761,7 @@ internal uint GetTokenValueAsUInt() string number = _token.Substring(2); return UInt32.Parse(number, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); } - //TODO NiSc + //TODO NiSc Check. //Check.Assert(false); return 0; } @@ -818,7 +817,7 @@ internal char ScanNextChar() } //else //{ - // //TODO NiSc + // //TOxDO NiSc NYI // //NYI: MacOS uses CR only // //Check.NotImplemented(); //} diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/LogMessages.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/LogMessages.cs index 20d4881c..31db0b11 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/LogMessages.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/LogMessages.cs @@ -28,6 +28,7 @@ public static partial void ArgbValueIsConsideredEmptyColor( // Differences in RTF vs Word (decimal tab) // Performance optimization + // TODO Use logging instead of Console.WriteLine. // TODO remove all Console.WriteLine calls. } } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Logging.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Logging.cs index 23809972..fb6428dd 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Logging.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Internals/Logging.cs @@ -24,9 +24,9 @@ public static class AppLogEvents // TODO Not yet used. - public static EventId FontCreated = new(1000, "Font created"); - public static EventId FontFound = new(1001, "Font found"); - public static EventId FontNotFound = new(1002, "Font not found"); + public static EventId FontCreated = new(AppLogEventIds.MDDOM + 0, "Font created"); + public static EventId FontFound = new(AppLogEventIds.MDDOM + 1, "Font found"); + public static EventId FontNotFound = new(AppLogEventIds.MDDOM + 2, "Font not found"); #if DEBUG_ diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/Axis.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/Axis.cs index b5d041dc..9b2de631 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/Axis.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/Axis.cs @@ -221,7 +221,7 @@ internal override void Serialize(Serializer serializer) var chartObject = Parent as Chart; Debug.Assert(chartObject != null); - serializer.WriteLine("\\" + chartObject.CheckAxis(this)); // HACK // BUG: What if Parent is not Chart? + serializer.WriteLine("\\" + chartObject.CheckAxis(this)); // Exception if Parent is not Chart. This is by design. var pos = serializer.BeginAttributes(); if (Values.MinimumScale is not null) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/AxisTitle.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/AxisTitle.cs index f9340d97..08523a8b 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/AxisTitle.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes.Charts/AxisTitle.cs @@ -123,7 +123,7 @@ internal override void Serialize(Serializer serializer) if (Values.Alignment is not null) serializer.WriteSimpleAttribute("Alignment", Alignment); - if (Values.VerticalAlignment is not null /*&& !Values.VerticalAlignment.IsNull*/) // BUG??? IsNull? + if (Values.VerticalAlignment is not null) serializer.WriteSimpleAttribute("VerticalAlignment", VerticalAlignment); if (Values.Caption is not null) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/Barcode.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/Barcode.cs index 8be42cc9..a5b8f7dd 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/Barcode.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/Barcode.cs @@ -76,7 +76,7 @@ public string Code } /// - /// ??? TODO + /// Ratio between narrow and wide lines. /// public double LineRatio { @@ -85,7 +85,7 @@ public double LineRatio } /// - /// ??? TODO + /// Height of lines. /// public double LineHeight { @@ -94,7 +94,7 @@ public double LineHeight } /// - /// ??? TODO + /// Width of a narrow line. /// public double NarrowLineWidth { diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/WrapFormat.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/WrapFormat.cs index a6ccb6cf..cd2b52a7 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/WrapFormat.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Shapes/WrapFormat.cs @@ -81,7 +81,7 @@ public Unit DistanceRight internal override void Serialize(Serializer serializer) { if (IsNull()) - return; // BUG??? Not detected by the caller anymore. + return; // IsNull called here so callers must not make this check. var pos = serializer.BeginContent("WrapFormat"); if (Values.Style is not null) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Cell.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Cell.cs index 3c9244c1..32010a43 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Cell.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Cell.cs @@ -45,9 +45,9 @@ internal Cell(DocumentObject parent) : base(parent) protected override object DeepCopy() { var cell = (Cell)base.DeepCopy(); - // Remove all references to the original object hierarchy. cell.ResetCachedValues(); - // TODO Call ResetCachedValues() for all classes where this is needed! + + // Remove all references to the original object hierarchy. if (cell.Values.Format != null) { cell.Values.Format = cell.Values.Format.Clone(); diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Table.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Table.cs index c57ecc55..5898315e 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Table.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Tables/Table.cs @@ -128,7 +128,7 @@ public void SetShading(int clm, int row, int clms, int rows, Color clr) /// Sets the borders surrounding the specified range of the table. /// public void SetEdge(int clm, int row, int clms, int rows, - Edge edge, BorderStyle style, Unit width, Color clr) // TODO: make Color? + Edge edge, BorderStyle style, Unit width, Color clr) { int maxRow = row + rows - 1; int maxClm = clm + clms - 1; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/ElementsExtensions.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/ElementsExtensions.cs index 4fadfe3b..5f261d99 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/ElementsExtensions.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/ElementsExtensions.cs @@ -112,7 +112,7 @@ public static IEnumerable GetElements(this DocumentObject documentObject, yield return element; if (element == null) // Null elements have no children. - continue; // BUG Or throw instead? + continue; var children = element.GetElementsRecursively(includeHeaderFooter); foreach (var child in children) @@ -135,7 +135,7 @@ public static IEnumerable GetElements(this DocumentObject documentObject, foreach (var element in elements) { var type = element?.GetType(); - var stop = type != null && stopAtElements.Contains(type); // BUG Or throw instead? + var stop = type != null && stopAtElements.Contains(type); if (stop && !includeStoppingElements) yield break; @@ -146,7 +146,7 @@ public static IEnumerable GetElements(this DocumentObject documentObject, yield break; if (element == null) // Null elements have no children. - continue; // BUG Or throw instead? + continue; var children = element.GetElementsRecursively(includeHeaderFooter); foreach (var child in children) diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/MergedCellList.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/MergedCellList.cs index 777a4427..a5a67c76 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/MergedCellList.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel.Visitors/MergedCellList.cs @@ -78,7 +78,7 @@ void Init(Table table) for (int clmIdx = 0; clmIdx < columns; ++clmIdx) { var cell = table[rwIdx, clmIdx]; - // TODO Make this smarter? + // TOxDO Make this smarter? if (!IsAlreadyCovered(cell)) Add(cell); } @@ -121,17 +121,50 @@ bool IsAlreadyCovered(Cell cell) public new Cell this[int index] => base[index]; /// - /// Gets a borders object that should be used for rendering. + /// Gets a borders object that should be used for RTF rendering. + /// In RTF heading rows must not be considered, as repeated heading row rendering is done in the RTF application. + /// Further, inserting rows later in the RTF application should not move or duplicate any heading row border formatting, + /// that could otherwise be assigned here to the following row, down to the table content. /// /// /// Thrown when the cell is not in this list. /// This situation occurs if the given cell is merged "away" by a previous one. /// - public Borders GetEffectiveBorders(Cell cell) + public Borders GetEffectiveBordersRtf(Cell cell) + { + return GetEffectiveBordersInternal(cell, false, null); + } + + /// + /// Gets a borders object that should be used for PDF rendering. + /// In PDF heading rows must be considered, as repeated heading row rendering is done in PDF rendering + /// Therefore the index of the last heading row (original, not repetition) must be committed. + /// + /// + /// Thrown when the cell is not in this list. + /// This situation occurs if the given cell is merged "away" by a previous one. + /// + public Borders GetEffectiveBordersPdf(Cell cell, int lastHeadingRow) + { + return GetEffectiveBordersInternal(cell, true, lastHeadingRow); + } + + Borders GetEffectiveBordersInternal(Cell cell, bool considerHeadingRows, int? consideredLastHeadingRow) { #if CACHE - if (_effectiveBordersLookup.TryGetValue(cell, out var result)) - return result; + var lastKnownHeaderInsertionRowIndex = GetLastKnownHeaderInsertionRowIndex(); + + // If a cached borders value exists for this cell, determine if it has to be determined again. + var existingLookupKey = _effectiveBordersLookup.TryGetValue(cell, out var result); + if (existingLookupKey) + { + // The cached borders object can be returned as no changes are expected, when... + if (!considerHeadingRows // ...not considering heading rows (considerHeadingRows should be always true for PDF and false for RTF rendering)... + || lastKnownHeaderInsertionRowIndex <= result.LastKnownHeaderInsertionRowIndex // ...or no heading has been inserted since the last borders determination... + || !_headerInsertionRowIndices.Contains(cell.Row.Index)) // ...or the changed _headerInsertionRowIndices does not contain this row + // (so this row is still not following a repeated heading row). + return result.Borders; + } #endif var borders = cell.Values.Borders; @@ -188,7 +221,28 @@ public Borders GetEffectiveBorders(Cell cell) borders.SetValue("Right", GetBorderFromBorders(nbrBrdrs, BorderType.Left)); } - var topNeighbor = GetNeighborTop(cellIdx); + + // If considering heading rows and if cellRowIndex is an index a header is been inserted at, override topRowIndex with the last heading row. + // This way for rows after a repeated heading row the original last heading row is considered as the top neighbor for top border determination + // instead of the last content row on the page before. + // Repetitions of the heading are not managed in MergedCellList (what would be wrong for RTF rendering), therefore jumping to the original header is necessary. + // This behaviour is wanted for PDF only, where all heading rows repetitions are rendered with their border formatting into the document and + // where their neighbors have to consider this. + int? topRowIndexOverride; + if (considerHeadingRows && _headerInsertionRowIndices.Contains(cellRowIndex)) + topRowIndexOverride = consideredLastHeadingRow; + else + topRowIndexOverride = null; + + var topNeighbor = GetNeighborTop(cellIdx, topRowIndexOverride); + + // If not considering heading rows and if the topNeighbor is a heading row, set it to null to ignore it for top border determination. + // This way the first content row's top border doesn't possibly get the bottom border of the original last heading row. + // This behaviour is wanted for RTF only, where all heading rows repetitions are rendered with their border formatting in the RTF application when displaying the + // document and where content row movement or insertion in the RTF application must not copy formatting values that actually belong to the original last heading row. + if (!considerHeadingRows && topNeighbor?.Row.HeadingFormat == true) + topNeighbor = null; + if (topNeighbor != null && topNeighbor.RoundedCorner != RoundedCorner.BottomLeft && topNeighbor.RoundedCorner != RoundedCorner.BottomRight) { var nbrBrdrs = topNeighbor.Values.Borders; @@ -196,6 +250,7 @@ public Borders GetEffectiveBorders(Cell cell) borders.SetValue("Top", GetBorderFromBorders(nbrBrdrs, BorderType.Bottom)); } + var bottomNeighbor = GetNeighborBottom(cellIdx); if (bottomNeighbor != null && bottomNeighbor.RoundedCorner != RoundedCorner.TopLeft && bottomNeighbor.RoundedCorner != RoundedCorner.TopRight) { @@ -204,12 +259,18 @@ public Borders GetEffectiveBorders(Cell cell) borders.SetValue("Bottom", GetBorderFromBorders(nbrBrdrs, BorderType.Top)); } #if CACHE - _effectiveBordersLookup.Add(cell, borders); + // Add or update cached borders and lastKnownHeaderInsertionRowIndex for cell. + _effectiveBordersLookup[cell] = (borders, lastKnownHeaderInsertionRowIndex); #endif return borders; } + #if CACHE - Dictionary _effectiveBordersLookup = new Dictionary(); + /// + /// A dictionary assigning the determined effective borders and the last known header insertion row index at that time to a cell for caching purposes. + /// If a new last heading insertion row is known, the cached borders may have to be determined again. + /// + readonly Dictionary _effectiveBordersLookup = new(); #endif /// @@ -314,7 +375,7 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) if (border == null || border.Values.Width.IsValueNullOrEmpty()) relevantDocObj = borders; - // TODO Avoid 'GetValue("'. + // TOxDO Avoid 'GetValue("'. // Avoid unnecessary GetValue calls. => Not trivial because it can be Border or Borders. object? visible = relevantDocObj!.GetValue("visible", GV.GetNull); // relevantDocObj cannot be null here. if (visible != null && !(bool)visible) @@ -387,16 +448,23 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) return null; } - Cell? GetNeighborTop(int cellIdx) + /// + /// Gets the top neighbor of a cell. + /// TopRowIndexOverride may be used to manually override the row index of the top neighbor row. + /// For PDF effective border determination in case of heading repetitions the lastHeadingRow index can be used to get a cell + /// in the original last heading row as neighbor instead of the last content row on the page before. + /// Repetitions of the heading are not managed in MergedCellList (what would be wrong for RTF rendering), therefore jumping to the original header is necessary. + /// + Cell? GetNeighborTop(int cellIdx, int? topRowIndexOverride = null) { - Cell cell = this[cellIdx]; + var cell = this[cellIdx]; if (cell.Row.Index == 0) return null; - for (int index = cellIdx - 1; index >= 0; --index) + for (var index = cellIdx - 1; index >= 0; --index) { var currCell = this[index]; - if (IsNeighborTop(cell, currCell)) + if (IsNeighborTop(cell, currCell, topRowIndexOverride)) return currCell; } return null; @@ -404,7 +472,7 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) Cell? GetNeighborLeft(int cellIdx) { - Cell cell = this[cellIdx]; + var cell = this[cellIdx]; if (cell.Column.Index == 0) return null; @@ -414,7 +482,7 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) if (cell2.Row.Index == cell.Row.Index) return cell2; } - for (int index = cellIdx - 2; index >= 0; --index) + for (var index = cellIdx - 2; index >= 0; --index) { var currCell = this[index]; if (IsNeighborLeft(cell, currCell)) @@ -425,7 +493,7 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) Cell? GetNeighborRight(int cellIdx) { - Cell cell = this[cellIdx]; + var cell = this[cellIdx]; if (cell.Column.Index + cell.MergeRight == cell.Table.Columns.Count - 1) return null; @@ -435,7 +503,7 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) if (cell2.Row.Index == cell.Row.Index) return cell2; } - for (int index = cellIdx + 2; index < Count; ++index) + for (var index = cellIdx + 2; index < Count; ++index) { var currCell = this[index]; if (IsNeighborRight(cell, currCell)) @@ -446,11 +514,11 @@ static Unit GetEffectiveBorderWidth(Borders? borders, BorderType type) Cell? GetNeighborBottom(int cellIdx) { - Cell cell = this[cellIdx]; + var cell = this[cellIdx]; if (cell.Row.Index + cell.MergeDown == cell.Table.Rows.Count - 1) return null; - for (int index = cellIdx + 1; index < Count; ++index) + for (var index = cellIdx + 1; index < Count; ++index) { var currCell = this[index]; if (IsNeighborBottom(cell, currCell)) @@ -501,44 +569,66 @@ bool IsNeighbor(Cell cell1, Cell cell2, NeighborPosition position) bool IsNeighborBottom(Cell cell1, Cell cell2) { - bool isNeighbor = false; - int bottomRowIdx = cell1.Row.Index + cell1.MergeDown + 1; + var bottomRowIdx = cell1.Row.Index + cell1.MergeDown + 1; var c1CI = cell1.Column.Index; var c2CI = cell2.Column.Index; - isNeighbor = cell2.Row.Index == bottomRowIdx && - c2CI <= c1CI && - c2CI + cell2.MergeRight >= c1CI; + var isNeighbor = cell2.Row.Index == bottomRowIdx && + c2CI <= c1CI && + c2CI + cell2.MergeRight >= c1CI; return isNeighbor; } bool IsNeighborLeft(Cell cell1, Cell cell2) { - bool isNeighbor = false; - int leftClmIdx = cell1.Column.Index - 1; - isNeighbor = cell2.Row.Index <= cell1.Row.Index && - cell2.Row.Index + cell2.MergeDown >= cell1.Row.Index && - cell2.Column.Index + cell2.MergeRight == leftClmIdx; + var leftClmIdx = cell1.Column.Index - 1; + var isNeighbor = cell2.Row.Index <= cell1.Row.Index && + cell2.Row.Index + cell2.MergeDown >= cell1.Row.Index && + cell2.Column.Index + cell2.MergeRight == leftClmIdx; return isNeighbor; } bool IsNeighborRight(Cell cell1, Cell cell2) { - bool isNeighbor = false; - int rightClmIdx = cell1.Column.Index + cell1.MergeRight + 1; - isNeighbor = cell2.Row.Index <= cell1.Row.Index && - cell2.Row.Index + cell2.MergeDown >= cell1.Row.Index && - cell2.Column.Index == rightClmIdx; + var rightClmIdx = cell1.Column.Index + cell1.MergeRight + 1; + var isNeighbor = cell2.Row.Index <= cell1.Row.Index && + cell2.Row.Index + cell2.MergeDown >= cell1.Row.Index && + cell2.Column.Index == rightClmIdx; return isNeighbor; } - bool IsNeighborTop(Cell cell1, Cell cell2) + /// + /// Returns true, if cell2 is the top neighbor of cell1. + /// TopRowIndexOverride may be used to manually override the top neighbor row index determined by cell1. + /// For PDF effective border determination in case of heading repetitions the lastHeadingRow index can be used to get a cell + /// in the original last heading row as neighbor instead of the last content row on the page before. + /// Repetitions of the heading are not managed in MergedCellList (what would be wrong for RTF rendering), therefore jumping to the original header is necessary. + /// + bool IsNeighborTop(Cell cell1, Cell cell2, int? topRowIndexOverride = null) { - int topRowIdx = cell1.Row.Index - 1; + var topRowIdx = topRowIndexOverride ?? cell1.Row.Index - 1; var c1CI = cell1.Column.Index; var c2CI = cell2.Column.Index; return cell2.Row.Index + cell2.MergeDown == topRowIdx && c2CI + cell2.MergeRight >= c1CI && c2CI <= c1CI; } + + + readonly SortedSet _headerInsertionRowIndices = new(); + + /// + /// After a heading has been inserted in PDF rendering, the index of the first following content row should be added here + /// to consider this heading when determining the effective borders. + /// + public void AddHeaderInsertionRowIndex(int rowIndex) + { + _headerInsertionRowIndices.Add(rowIndex); + } + + int GetLastKnownHeaderInsertionRowIndex() + { + return _headerInsertionRowIndices.Any() ? _headerInsertionRowIndices.Last() : -1; + } + } } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Borders.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Borders.cs index c8c493bf..2d694c90 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Borders.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Borders.cs @@ -391,7 +391,7 @@ internal void Serialize(Serializer serializer, Borders? refBorders) //if (!IsNull("DiagonalDown")) if (!Values.DiagonalDown.IsValueNullOrEmpty()) - Values.DiagonalUp!.Serialize(serializer, "DiagonalDown", null); + Values.DiagonalDown!.Serialize(serializer, "DiagonalDown", null); //if (!IsNull("DiagonalUp")) if (!Values.DiagonalUp.IsValueNullOrEmpty()) @@ -420,58 +420,59 @@ internal void Serialize(Serializer serializer, Borders? refBorders) return null; } -// /// -// /// Returns an enumerator that can iterate through the Borders. -// /// -// public class BorderEnumerator : IEnumerator -// { -//#warning This class must be checked with a unit test. -// /// -// /// Creates a new BorderEnumerator. -// /// -// public BorderEnumerator(Dictionary ht) -// { -// _ht = ht; -// _index = -1; -// } - -// public void Dispose() -// => throw new NotImplementedException(); - -// /// -// /// Sets the enumerator to its initial position, which is before the first element in the border collection. -// /// -// public void Reset() => _index = -1; - -// object IEnumerator.Current => Current; - -// /// -// /// Gets the current element in the border collection. -// /// -// public Border Current -// { -// get -// { -// IEnumerator enumerator = _ht.GetEnumerator(); -// enumerator.Reset(); -// for (int idx = 0; idx < _index + 1; idx++) -// enumerator.MoveNext(); -// return (((DictionaryEntry)enumerator.Current).Value as Border)!; // B_UG: May return null -// } -// } - -// /// -// /// Advances the enumerator to the next element of the border collection. -// /// -// public bool MoveNext() -// { -// _index++; -// return (_index < _ht.Count); -// } - -// int _index; -// readonly Dictionary _ht; -// } + // /// + // /// Returns an enumerator that can iterate through the Borders. + // /// + // public class BorderEnumerator : IEnumerator + // { + //#warning This class must be checked with a unit test. + // /// + // /// Creates a new BorderEnumerator. + // /// + // public BorderEnumerator(Dictionary ht) + // { + // _ht = ht; + // _index = -1; + // } + + // public void Dispose() + // => throw new NotImplementedException(); + + // /// + // /// Sets the enumerator to its initial position, which is before the first element in the border collection. + // /// + // public void Reset() => _index = -1; + + // object IEnumerator.Current => Current; + + // /// + // /// Gets the current element in the border collection. + // /// + // public Border Current + // { + // get + // { + // IEnumerator enumerator = _ht.GetEnumerator(); + // enumerator.Reset(); + // for (int idx = 0; idx < _index + 1; idx++) + // enumerator.MoveNext(); + // // return (((DictionaryEntry)enumerator.Current).Value as Border)!; // B_UG: May return null + // return (((KeyValuePair)enumerator.Current).Value as Border)!; + // } + // } + + // /// + // /// Advances the enumerator to the next element of the border collection. + // /// + // public bool MoveNext() + // { + // _index++; + // return (_index < _ht.Count); + // } + + // int _index; + // readonly Dictionary _ht; + // } /// /// Returns the meta object of this instance. diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Document.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Document.cs index 4ad7fc89..737f18a7 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Document.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Document.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the solution root for more information. using System.Diagnostics.CodeAnalysis; +using MigraDoc.DocumentObjectModel.Tables; using MigraDoc.DocumentObjectModel.Visitors; namespace MigraDoc.DocumentObjectModel @@ -133,6 +134,27 @@ public Section LastSection } } + /// + /// Returns the last table in the document, or null if no table exists. + /// + public Table LastTable + { + get + { + var sections = Values.Sections; + if (sections is null) + return null!; + for (int idx = sections.Count - 1; idx >= 0; --idx) + { + var section = sections[idx]; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (section.LastTable is not null) + return section.LastTable; + } + return null!; + } + } + /// /// Gets or sets a comment associated with this object. /// diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs index eb97efd0..5329c36c 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Font.cs @@ -111,10 +111,10 @@ public void ApplyFont(Font font) else if (font.Values.Superscript is not null) Superscript = font.Superscript; - if (Values.Underline is not null) + if (font.Values.Underline is not null) Underline = font.Underline; - if (!Values.Color.IsValueNullOrEmpty()) + if (!font.Values.Color.IsValueNullOrEmpty()) Color = font.Color; } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/FormattedText.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/FormattedText.cs index 054e9c90..b48a2f2e 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/FormattedText.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/FormattedText.cs @@ -139,7 +139,7 @@ public Footnote AddFootnote() /// Adds a text phrase to the formatted text. /// /// Content of the new text object. - /// Returns a new Text object. + /// Returns a new Text object with the last element of text that was added. public Text AddText(string text) => Elements.AddText(text); diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Paragraph.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Paragraph.cs index aa1e1baa..31a85402 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Paragraph.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Paragraph.cs @@ -56,7 +56,8 @@ protected override object DeepCopy() /// /// Adds a text phrase to the paragraph. /// - public Text AddText(string text) // TODO: Update docu according to Elements.AddText. Update all places in project. StL: ??? what? + /// Returns a new Text object with the last element of text that was added. + public Text AddText(string text) => Elements.AddText(text); /// diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/ParagraphElements.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/ParagraphElements.cs index 0880e572..0d41a8ef 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/ParagraphElements.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/ParagraphElements.cs @@ -43,12 +43,12 @@ internal ParagraphElements(DocumentObject parent) : base(parent) /// The function returns the last text object that was created. /// /// Contents of the new Text objects. - /// Returns a new Text object. + /// Returns a new Text object with the last element of text that was added. public Text AddText(string text) { if (text == null) throw new ArgumentNullException(nameof(text)); - Text txt = default!; + Text result = default!; string[] lines = text.Split('\n'); int lineCount = lines.Length; for (int line = 0; line < lineCount; line++) @@ -59,8 +59,7 @@ public Text AddText(string text) { if (tabParts[idx].Length != 0) { - txt = new Text(tabParts[idx]); - Add(txt); + Add(result = new(tabParts[idx])); } if (idx < count - 1) AddTab(); @@ -68,7 +67,7 @@ public Text AddText(string text) if (line < lineCount - 1) AddLineBreak(); } - return txt; + return result; } /// @@ -303,24 +302,24 @@ public Hyperlink AddHyperlink(string filename, string bookmarkName, HyperlinkTar /// Defines if the HyperlinkType ExternalBookmark shall be opened in a new window. /// If not set, the viewer application should behave in accordance with the current user preference. public Hyperlink AddHyperlinkToEmbeddedDocument(string destinationPath, HyperlinkTargetWindow newWindow = HyperlinkTargetWindow.UserPreference) - => AddHyperlinkToEmbeddedDocument(null, destinationPath, newWindow); + => AddHyperlinkToEmbeddedDocument("", destinationPath, newWindow); /// /// Adds a new Hyperlink of Type "EmbeddedDocument". /// The target is a Bookmark in an embedded Document in an external PDF Document. /// - /// The path to the target document. + /// The path to the target document. Can be empty if target is an embedded document in the current document. /// The path to the named destination through the embedded documents in the target document. /// The path is separated by '\' and the last segment is the name of the named destination. /// The other segments describe the route from the root document to the embedded document. /// Each segment name refers to a child with this name in the EmbeddedFiles name dictionary. /// Defines if the HyperlinkType ExternalBookmark shall be opened in a new window. /// If not set, the viewer application should behave in accordance with the current user preference. - public Hyperlink AddHyperlinkToEmbeddedDocument(string? filename, string destinationPath, HyperlinkTargetWindow newWindow = HyperlinkTargetWindow.UserPreference) - { // BUG "filename" nullable? + public Hyperlink AddHyperlinkToEmbeddedDocument(string filename, string destinationPath, HyperlinkTargetWindow newWindow = HyperlinkTargetWindow.UserPreference) + { var hyperlink = new Hyperlink { - Name = filename ?? "", + Name = filename, BookmarkName = destinationPath, NewWindow = newWindow, Type = HyperlinkType.EmbeddedDocument diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs index 67a9abec..37cafb8f 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.DocumentObjectModel/DocumentObjectModel/Style.cs @@ -78,7 +78,7 @@ protected override object DeepCopy() } /// - /// Indicates whether the style is read-only. + /// Indicates whether the style is read-only. /// public bool IsReadOnly { @@ -88,7 +88,7 @@ public bool IsReadOnly bool _readOnly; /// - /// Gets the font of ParagraphFormat. + /// Gets the font of ParagraphFormat. /// Calling style.Font is just a shortcut to style.ParagraphFormat.Font. /// public Font Font // TODO: Move to Values? @@ -166,7 +166,7 @@ public string BaseStyle // styles cannot be null if idxBaseStyle >= 0. Debug.Assert(styles != null, nameof(styles) + " != null"); - // Is this style in the base style chain of the new base style. + // Is this style in the base style chain of the new base style? var style = styles[idxBaseStyle]; while (style != null) { @@ -322,7 +322,7 @@ internal override void Serialize(Serializer serializer) // Note: we must write "Underline = none" if the base style has "Underline = single" - we cannot // detect this if we compare with the built-in style that has no underline. // Known problem: Default values like "OutlineLevel = Level1" will now be serialized. - // TODO: optimize... + // TODO: Optimize DDL output, remove redundant default values. } else { diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/ParagraphRenderer.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/ParagraphRenderer.cs index b0c4bf39..ebbcb35c 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/ParagraphRenderer.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/ParagraphRenderer.cs @@ -199,11 +199,11 @@ string GetOutlineTitle() var iter = new ParagraphIterator(_paragraph.Elements); iter = iter.GetFirstLeaf(); - bool ignoreBlank = true; - string title = ""; + var ignoreBlank = true; + var title = ""; while (iter != null) { - DocumentObject current = iter.Current; + var current = iter.Current; if (!ignoreBlank && (IsBlank(current) || IsTab(current) || IsLineBreak(current))) { title += " "; @@ -224,9 +224,6 @@ string GetOutlineTitle() title += GetSymbol((Character)current); ignoreBlank = false; } - - if (title.Length > 64) - break; iter = iter.GetNextLeaf(); } return title; @@ -1485,6 +1482,9 @@ internal override void Format(Area area, FormatInfo? previousFormatInfo) if (formatInfo.IsEnding && lastResult != FormatResult.NewLine) StoreLineInformation(); + if (formatInfo.IsEnding) + StoreBottomBorderInformation(); + formatInfo.ImageRenderInfos = _imageRenderInfos; FinishLayoutInfo(); } @@ -2239,14 +2239,6 @@ void StoreLineInformation() else contentArea = contentArea.Unite(_formattingArea.GetFittingRect(_currentYPosition, _currentVerticalInfo.Height) ?? NRT.ThrowOnNull()); - XUnit bottomBorderOffset = BottomBorderOffset; - if (bottomBorderOffset > 0) - { - if (contentArea is null) - NRT.ThrowOnNull(); - contentArea = contentArea.Unite(_formattingArea.GetFittingRect(_currentYPosition + _currentVerticalInfo.Height, bottomBorderOffset) ?? NRT.ThrowOnNull()); - } - var lineInfo = new LineInfo(); lineInfo.Vertical = _currentVerticalInfo; @@ -2276,6 +2268,20 @@ void StoreLineInformation() ((ParagraphFormatInfo)_renderInfo.FormatInfo).AddLineInfo(lineInfo); } + /// + /// Adds the BottomBorderOffset to the ContentArea. This should only be called for the last line of a paragraph. + /// + void StoreBottomBorderInformation() + { + var contentArea = _renderInfo.LayoutInfo.ContentArea; + + var bottomBorderOffset = BottomBorderOffset; + if (bottomBorderOffset > 0) + contentArea = contentArea.Unite(_formattingArea.GetFittingRect(_currentYPosition + _currentVerticalInfo.Height, bottomBorderOffset) ?? NRT.ThrowOnNull()); + + _renderInfo.LayoutInfo.ContentArea = contentArea; + } + /// /// Gets the top border offset for the first line, else 0. /// diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/TableRenderer.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/TableRenderer.cs index 50bb3e6c..c5c3f2bf 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/TableRenderer.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/TableRenderer.cs @@ -78,6 +78,10 @@ void RenderHeaderRows() else break; // Exit the loop when we hit the first non-header cell. _mergedCells is sorted. } + + // _startRow is the index of the first content row on the page. Add this index as header insertion row index to _mergedCells, + // to enable considering repeated headers on PDF effective border determination in MergedCellList. + _mergedCells.AddHeaderInsertionRowIndex(_startRow); } void RenderCell(Cell cell) @@ -146,7 +150,7 @@ void RenderBorders(Cell cell, Rectangle innerRect) XUnit rightPos = leftPos + innerRect.Width; XUnit topPos = innerRect.Y; XUnit bottomPos = innerRect.Y + innerRect.Height; - var mergedBorders = _mergedCells.GetEffectiveBorders(cell); + var mergedBorders = _mergedCells.GetEffectiveBordersPdf(cell, _lastHeaderRow); var bordersRenderer = new BordersRenderer(mergedBorders, _gfx); XUnit bottomWidth = bordersRenderer.GetWidth(BorderType.Bottom); @@ -235,7 +239,7 @@ void RenderContent(Cell cell, Rectangle innerRect) Rectangle GetInnerRect(XUnit startingHeight, Cell cell) { - var bordersRenderer = new BordersRenderer(_mergedCells.GetEffectiveBorders(cell), _gfx); + var bordersRenderer = new BordersRenderer(_mergedCells.GetEffectiveBordersPdf(cell, _lastHeaderRow), _gfx); var formattedCell = _formattedCells[cell]; XUnit width = formattedCell.InnerWidth; @@ -371,7 +375,7 @@ void FormatCells() { var cell = _mergedCells[index]; FormattedCell formattedCell = new FormattedCell(cell, _documentRenderer, - _mergedCells.GetEffectiveBorders(cell), + _mergedCells.GetEffectiveBordersPdf(cell, _lastHeaderRow), _fieldInfos, 0, 0); formattedCell.Format(_gfx); _formattedCells.Add(cell, formattedCell); @@ -530,7 +534,7 @@ XUnit LeftBorderOffset { if (_table.Rows.Count > 0 && _table.Columns.Count > 0) { - var borders = _mergedCells.GetEffectiveBorders(_table[0, 0]); + var borders = _mergedCells.GetEffectiveBordersPdf(_table[0, 0], _lastHeaderRow); var bordersRenderer = new BordersRenderer(borders, _gfx); _leftBorderOffset = bordersRenderer.GetWidth(BorderType.Left); } @@ -873,7 +877,7 @@ void CreateNextBottomBorderPosition(ref int skipIndex, ref int lastBorderRow) /// The calculated border width. XUnit CalcBottomBorderWidth(Cell cell) { - var borders = _mergedCells.GetEffectiveBorders(cell); + var borders = _mergedCells.GetEffectiveBordersPdf(cell, _lastHeaderRow); if (borders != null) { BordersRenderer bordersRenderer = new BordersRenderer(borders, _gfx); diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ElementAlignment.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ElementAlignment.cs index 33875b95..4c43ffc1 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ElementAlignment.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ElementAlignment.cs @@ -9,27 +9,27 @@ namespace MigraDoc.Rendering enum ElementAlignment { /// - /// TODO Default + /// Element is aligned near. This is the default. /// Near = 0, /// - /// TODO + /// Element is center-aligned. /// Center, /// - /// TODO + /// Element is far-aligned. /// Far, /// - /// TODO + /// Element is inside-aligned. /// Inside, /// - /// TODO + /// Element is outside-aligned. /// Outside } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/Floating.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/Floating.cs index 1f86484c..544e6807 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/Floating.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/Floating.cs @@ -9,7 +9,7 @@ namespace MigraDoc.Rendering enum Floating { /// - /// TODO Default + /// The element floats from top to bottom. This is the default. /// TopBottom = 0, @@ -18,20 +18,20 @@ enum Floating /// None, - // Served for future extensions: - + // Reserved for future extensions: + /// - /// TODO + /// Reserved for future extensions. The element floats from left to right. /// Left, - + /// - /// TODO + /// Reserved for future extensions. The element floats from right to left. /// Right, /// - /// TODO + /// Reserved for future extensions. /// BothSides, } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/HorizontalReference.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/HorizontalReference.cs index fc12204d..b0e6f194 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/HorizontalReference.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/HorizontalReference.cs @@ -9,17 +9,17 @@ namespace MigraDoc.Rendering enum HorizontalReference { /// - /// TODO + /// Horizontal reference is the area boundary. /// AreaBoundary = 0, // Default /// - /// TODO + /// Horizontal reference is the page margin. /// PageMargin, /// - /// TODO + /// Horizontal reference is the page. /// Page } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ImageFailure.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ImageFailure.cs index fd88f3ea..c883cd79 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ImageFailure.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/ImageFailure.cs @@ -6,27 +6,27 @@ namespace MigraDoc.Rendering enum ImageFailure { /// - /// TODO + /// No failure has occurred. /// None = 0, /// - /// TODO + /// Image file was not found. /// FileNotFound, /// - /// TODO + /// Image type is not supported. /// InvalidType, /// - /// TODO + /// Image could not be read. /// NotRead, /// - /// TODO + /// Image has empty or invalid size. /// EmptySize } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/VerticalReference.cs b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/VerticalReference.cs index 90bf1e61..03c56496 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/VerticalReference.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.Rendering/Rendering/enums/VerticalReference.cs @@ -6,22 +6,22 @@ namespace MigraDoc.Rendering enum VerticalReference { /// - /// TODO + /// Vertical reference is the previous element. /// PreviousElement = 0, // Default - + /// - /// TODO + /// Vertical reference is the area boundary. /// AreaBoundary, /// - /// TODO + /// Vertical reference is the page margin. /// PageMargin, /// - /// TODO + /// Vertical reference is page. /// Page } diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/CellFormatRenderer.cs b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/CellFormatRenderer.cs index 158526a7..a452157a 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/CellFormatRenderer.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/CellFormatRenderer.cs @@ -27,7 +27,7 @@ internal override void Render() { _useEffectiveValue = true; _coveringCell = _cellList.GetCoveringCell(_cell)!; - var borders = _cellList.GetEffectiveBorders(_coveringCell); + var borders = _cellList.GetEffectiveBordersRtf(_coveringCell); if (_cell.Column!.Index != _coveringCell.Column!.Index) // The "!" are needed here as some properties may be null if DOM objects are not added to a document. return; diff --git a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfWriter.cs b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfWriter.cs index 7306f102..5854e7d7 100644 --- a/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfWriter.cs +++ b/src/foundation/src/MigraDoc/src/MigraDoc.RtfRendering/RtfRendering/RtfWriter.cs @@ -136,7 +136,7 @@ static bool IsCp1252Char(char ch) public void WriteHex(uint hex) { if (hex > 0xFF) - //TODO: Fehlermeldung + //TODO: Error message? Debug.Assert? return; _textWriter.Write(@"\'" + hex.ToString("x")); diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/ParagraphTests.cs b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/ParagraphTests.cs index e6f470f3..74a2fda3 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/ParagraphTests.cs +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/ParagraphTests.cs @@ -1,7 +1,10 @@ -using PdfSharp.Snippets.Font; +using MigraDoc.DocumentObjectModel.Shapes; +using PdfSharp.Snippets.Font; using PdfSharp.TestHelper; using MigraDoc.Rendering; using PdfSharp.Fonts; +using PdfSharp.Pdf; +using PdfSharp.Pdf.IO; using Xunit; namespace MigraDoc.DocumentObjectModel.Tests @@ -50,5 +53,166 @@ public void Test_Empty_FormattedText() pdfRenderer.PdfDocument.Save(filename); PdfFileHelper.StartPdfViewerIfDebugging(filename); } + + /// + /// Creates a series of documents with a paragraph containing several lines and top and bottom border. + /// Another paragraph with a height increasing from document to document, moves the test paragraph by this offset + /// and forces a page break at different positions. + /// + [Fact] + public void Test_Multiline_Border_Paragraph_PageBreaks() + { +#if CORE + GlobalFontSettings.FontResolver = NewFontResolver.Get(); +#endif + // Create one document containing all results. + var sumDoc = new PdfDocument(PdfFileHelper.CreateTempFileName("Test_Multiline_Border_Paragraph_PageBreaks")); + var exceptions = new List(); + + + // Create documents for different offsets. + for (var offset = 15; offset <= 95; offset += 5) + { + // As the original problem, this test is written for, occurred only if there was only one word after the line breaks, we try it separately with one and multiple words. + for (var i = 0; i < 2; i++) + { + var isOneWord = i == 0; + var oneOrMultipleWordsStr = (isOneWord ? "one word" : "multiple words") + " after line breaks"; + + try + { + var document = new Document(); + + var style = document.Styles[StyleNames.Heading1]; + style!.Font.Size = Unit.FromPoint(14); + style.Font.Bold = true; + + var section = document.AddSection(); + + // Leave 10 cm for content. + section.PageSetup.TopMargin = Unit.FromCentimeter(5); + section.PageSetup.BottomMargin= Unit.FromCentimeter(14.7); + + // Add informational content. + var tf = section.AddTextFrame(); + tf.RelativeHorizontal = RelativeHorizontal.Margin; + tf.RelativeVertical = RelativeVertical.Margin; + tf.Top = Unit.FromCentimeter(-1.5); + tf.Left = 0; + tf.Height = Unit.FromCentimeter(1); + tf.Width = Unit.FromCentimeter(16); + tf.FillFormat.Color = Colors.LightGreen; + var p = tf.AddParagraph($"Offset: {offset} mm, {oneOrMultipleWordsStr}"); + p.Style = StyleNames.Heading1; + + tf = section.AddTextFrame(); + tf.RelativeHorizontal = RelativeHorizontal.Margin; + tf.RelativeVertical = RelativeVertical.Margin; + tf.Top = 0; + tf.Left = Unit.FromCentimeter(-1.5); + tf.Height = Unit.FromCentimeter(10); + tf.Width = Unit.FromCentimeter(0.5); + tf.FillFormat.Color = Colors.LightGreen; + tf.Orientation = TextOrientation.Upward; + p = tf.AddParagraph("10 cm space for content"); + p.Format.Alignment = ParagraphAlignment.Center; + + // Add offset inserting paragraph. + p = section.AddParagraph($"Paragraph to achieve an offset of {offset} mm."); + p.Format.LineSpacingRule = LineSpacingRule.Exactly; + p.Format.LineSpacing = Unit.FromMillimeter(offset); + p.Format.Shading.Color = Colors.LightGray; + + // Add test paragraph. + var spaceOrNoSpace = isOneWord ? "" : " "; + p = section.AddParagraph("Paragraph with four 1 cm lines and 1.5 cm top (green) and bottom (red) border."); + p.AddLineBreak(); + p.AddText($"Second{spaceOrNoSpace}line"); + p.AddLineBreak(); + p.AddText($"Third{spaceOrNoSpace}line"); + p.AddLineBreak(); + p.AddText($"Fourth{spaceOrNoSpace}line"); + p.Format.LineSpacingRule = LineSpacingRule.Exactly; + p.Format.LineSpacing = Unit.FromCentimeter(1); + p.Format.Shading.Color = Colors.LightBlue; + + // Set test paragraph borders. + var border = p.Format.Borders.Top; + border.Width = Unit.FromCentimeter(1.5); + border.Color = Colors.Green; + + border = p.Format.Borders.Bottom; + border.Width = Unit.FromCentimeter(1.5); + border.Color = Colors.Red; + + // Render document and add it to sumDoc. + var pdfRenderer = new PdfDocumentRenderer { Document = document }; + pdfRenderer.RenderDocument(); + + var stream = new MemoryStream(); + pdfRenderer.PdfDocument.Save(stream); + + var pdfDocument = PdfReader.Open(stream, PdfDocumentOpenMode.Import); + + foreach (var page in pdfDocument.Pages) + sumDoc.AddPage(page); + + // Always add an even count of pages for better comparability in PDF reader. + if (pdfDocument.PageCount % 2 == 1) + sumDoc.AddPage(); + + } + catch (Exception e) + { + var message = $"Exception while generating test document with {offset} mm offset and {oneOrMultipleWordsStr}."; + + // Add exception to list to continue tests and throw one AggregatedException at the end. + exceptions.Add(new Exception(message, e)); + + // Create temporary document with the exception and stacktrace. + var document = new Document(); + + var style = document.Styles[StyleNames.Normal]; + style!.Font.Color = Colors.Red; + style.ParagraphFormat.SpaceAfter = Unit.FromMillimeter(5); + + style = document.Styles[StyleNames.Heading1]; + style!.Font.Size = Unit.FromPoint(14); + style.Font.Bold = true; + + var section = document.AddSection(); + var p = section.AddParagraph(message); + p.Style = StyleNames.Heading1; + section.AddParagraph(e.Message); + section.AddParagraph(e.StackTrace ?? "Empty stacktrace"); + + // Render document and add it to sumDoc. + var pdfRenderer = new PdfDocumentRenderer { Document = document }; + pdfRenderer.RenderDocument(); + + var stream = new MemoryStream(); + pdfRenderer.PdfDocument.Save(stream); + + var pdfDocument = PdfReader.Open(stream, PdfDocumentOpenMode.Import); + + foreach (var page in pdfDocument.Pages) + sumDoc.AddPage(page); + + // Always add an even count of pages for better comparability in PDF reader. + if (pdfDocument.PageCount % 2 == 1) + sumDoc.AddPage(); + } + } + } + + // Save sumDoc. + var filename = PdfFileHelper.CreateTempFileName("Test_Multiline_Border_Paragraph_PageBreaks"); + sumDoc.Save(filename); + PdfFileHelper.StartPdfViewerIfDebugging(filename); + + // Finally throw occurred exceptions. + if (exceptions.Any()) + throw new AggregateException(exceptions); + } } } diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/TableTests.cs b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/TableTests.cs index d62b51c1..2256e986 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/TableTests.cs +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.DocumentObjectModel.Tests/TableTests.cs @@ -80,6 +80,14 @@ static Document CreateDocument() return document; } + PdfDocumentRenderer CreateReadablePdfDocumentRenderer(Document document) + { + var pdfRenderer = new PdfDocumentRenderer { Document = document }; + pdfRenderer.PdfDocument = new PdfDocument(); + pdfRenderer.PdfDocument.Options.CompressContentStreams = false; + return pdfRenderer; + } + [Fact] public void Test_MergeDown_Simple() { @@ -104,7 +112,7 @@ public void Test_MergeDown_Simple() var row1 = table.AddRow(); row1[1].AddParagraph("Row 1 Cell 1"); - var pdfRenderer = new PdfDocumentRenderer { Document = document }; + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); pdfRenderer.RenderDocument(); var filename = PdfFileHelper.CreateTempFileName("Test_MergeDown"); @@ -151,7 +159,7 @@ public void Test_KeepWith_MergeDown_PageBreak() dataRow2[0].AddParagraph("Item 2 Cell 0"); dataRow2[1].AddParagraph("Item 2 Cell 1"); - var pdfRenderer = new PdfDocumentRenderer { Document = document }; + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); pdfRenderer.RenderDocument(); var filename = PdfFileHelper.CreateTempFileName("Test_MergeDown"); @@ -193,7 +201,7 @@ public void Test_MergeDown_LineBreak_RowHeight() var row2CommentRow = table.AddRow(); row2CommentRow[1].AddParagraph("Comment 2 Cell 1"); - var pdfRenderer = new PdfDocumentRenderer { Document = document }; + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); pdfRenderer.RenderDocument(); var filename = PdfFileHelper.CreateTempFileName("Test_MergeDown_LineBreak_RowHeight"); @@ -332,7 +340,7 @@ public void Test_Border_Inheritance() dataRow1[1].AddParagraph("Item 1 Cell 1"); dataRow1[2].AddParagraph("Item 1 Cell 2"); - var pdfRenderer = new PdfDocumentRenderer { Document = document }; + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); pdfRenderer.RenderDocument(); var filename = PdfFileHelper.CreateTempFileName("Test_Border_Inheritance"); @@ -421,12 +429,129 @@ public void Test_Huge_MergeDown_Cell() dataRow2 = table.AddRow(); dataRow2[0].AddParagraph("Item 2 Cell 0"); dataRow2[1].AddParagraph("Item 2 Cell 1"); - var pdfRenderer = new PdfDocumentRenderer { Document = document }; + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); pdfRenderer.RenderDocument(); var filename = PdfFileHelper.CreateTempFileName("Test_Huge_MergeDown_Cell"); pdfRenderer.PdfDocument.Save(filename); PdfFileHelper.StartPdfViewerIfDebugging(filename); } + + [Fact] + public void Test_Repeated_Heading_Border() + { +#if CORE + GlobalFontSettings.FontResolver = NewFontResolver.Get(); +#endif + var bottomWidth = Unit.FromPoint(2.3); + var bottomColor = Colors.Blue; + var contentStreamBottomWidth = "2.3 w"; + var contentStreamBottomColor = "0 0 1 RG"; + + var headingBottomWidth = Unit.FromPoint(4.6); + var headingBottomColor = Colors.Red; + var contentStreamHeadingBottomWidth = "4.6 w"; + var contentStreamHeadingBottomColor = "1 0 0 RG"; + + var document = new Document(); + var section = document.AddSection(); + + var table = section.AddTable(); + table.Borders.Bottom.Width = bottomWidth; + table.Borders.Bottom.Color = bottomColor; + + table.AddColumn(Unit.FromCentimeter(16)); + + var headingRow = table.AddRow(); + headingRow.HeadingFormat = true; + headingRow.Cells[0].AddParagraph("Heading"); + headingRow.Borders.Bottom.Width = headingBottomWidth; + headingRow.Borders.Bottom.Color = headingBottomColor; + + // Add 4 rows with a height forcing a page break after the first two rows. + for (var rowNr = 1; rowNr <= 4; rowNr++) + { + var row = table.AddRow(); + row.Cells[0].AddParagraph($"Row {rowNr}"); + row.Height = Unit.FromCentimeter(10); + } + + var pdfRenderer = CreateReadablePdfDocumentRenderer(document); + pdfRenderer.RenderDocument(); + + var filename = PdfFileHelper.CreateTempFileName("Test_Repeated_Heading_Border"); + pdfRenderer.PdfDocument.Save(filename); + PdfFileHelper.StartPdfViewerIfDebugging(filename); + + + // Analyze the drawn border widths and colors in the PDF's pages content streams. + // The two parts the page break breaks the table into should be identical (except the row numbers). + for (var pageIdx = 0; pageIdx < pdfRenderer.PageCount; pageIdx++) + { + var page = pdfRenderer.PdfDocument.Pages[pageIdx]; + var contentReference = (PdfReference)page.Contents.Elements.Items[0]; + var content = (PdfDictionary)contentReference.Value; + var contentStream = content.Stream.ToString(); + + // Split ContentStream where the "Row" text is rendered. + var contentByRows = contentStream.Split("Td <00350052005A> Tj"); + contentByRows.Length.Should().Be(3, "as \"Row\" occurs twice per page, the stream should be splitted into 3 parts"); + + var rowsByDrawLinesByLines = contentByRows // Content split by "Row" text ... + .Select(r => r.Split(" l\n") // ... and that parts split by drawn lines ... + .Select(drawLine => drawLine.Split("\n")).ToArray() // ... and that parts split by line breaks. + ).ToArray(); + + + // Heading row. + var contentRowDrawLineParts = rowsByDrawLinesByLines[0]; + contentRowDrawLineParts.Length.Should().Be(2, "for the heading row only one bottom border should split the content into 2 parts"); + + // The part before the first draw line contains the data for the bottom border. + var bottomBorderDrawLinePartLines = contentRowDrawLineParts[0]; + bottomBorderDrawLinePartLines.Should().Contain(contentStreamHeadingBottomWidth, "heading bottom border should be of heading bottom border width"); + bottomBorderDrawLinePartLines.Should().Contain(contentStreamHeadingBottomColor, "heading bottom border should be of heading bottom border color"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomWidth, "heading bottom border should not be of content bottom border width"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomColor, "heading bottom border should not be of content bottom border color"); + + + // Row 1. + contentRowDrawLineParts = rowsByDrawLinesByLines[1]; + contentRowDrawLineParts.Length.Should().Be(3, "for the content rows one bottom and one top border should split the content into 3 parts"); + + // The part before the first draw line contains the data for the bottom border. + bottomBorderDrawLinePartLines = contentRowDrawLineParts[0]; + bottomBorderDrawLinePartLines.Should().Contain(contentStreamBottomWidth, "row 1 bottom border should be of content bottom border width"); + bottomBorderDrawLinePartLines.Should().Contain(contentStreamBottomColor, "row 1 bottom border should be of content bottom border color"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomWidth, "row 1 bottom border should not be of heading bottom border width"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomColor, "row 1 bottom border should not be of heading bottom border color"); + + // The part before the second draw line contains the data for the top border. Attention: Row 1 top border is equal to heading bottom border and should therefore have its values. + bottomBorderDrawLinePartLines = contentRowDrawLineParts[1]; + bottomBorderDrawLinePartLines.Should().Contain(contentStreamHeadingBottomWidth, "row 1 top border should be of heading bottom border width, as this is the same border"); + bottomBorderDrawLinePartLines.Should().Contain(contentStreamHeadingBottomColor, "row 1 top border should be of heading bottom border color, as this is the same border"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomWidth, "row 1 top border should not be of content bottom border width, as this border is the same like heading bottom"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomColor, "row 1 top border should not be of content bottom border color, as this border is the same like heading bottom"); + + + // Row 2. + contentRowDrawLineParts = rowsByDrawLinesByLines[2]; + contentRowDrawLineParts.Length.Should().Be(3, "for the content rows one bottom and one top border should split the content into 3 parts"); + + // The part before the first draw line contains the data for the bottom border. + bottomBorderDrawLinePartLines = contentRowDrawLineParts[0]; + bottomBorderDrawLinePartLines.Should().Contain(contentStreamBottomWidth, "row 2 bottom border should be of content bottom border width"); + bottomBorderDrawLinePartLines.Should().Contain(contentStreamBottomColor, "row 2 bottom border should be of content bottom border color"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomWidth, "row 2 bottom border should not be of heading bottom border width"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomColor, "row 2 bottom border should not be of heading bottom border color"); + + // The part before the second draw line contains the data for the top border. Attention: This should not be set as the values of the bottom border should remain unchanged. + bottomBorderDrawLinePartLines = contentRowDrawLineParts[1]; + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomWidth, "row 2 top border should not be set as the values should be the same as for the bottom border"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamBottomColor, "row 2 top border should not be set as the values should be the same as for the bottom border"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomWidth, "row 2 top border should not be set as the values should be the same as for the bottom border"); + bottomBorderDrawLinePartLines.Should().NotContain(contentStreamHeadingBottomColor, "row 2 top border should not be set as the values should be the same as for the bottom border"); + } + } } } diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-gdi/MigraDoc.Tests-gdi.csproj b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-gdi/MigraDoc.Tests-gdi.csproj index ff7e67ca..f15b34c2 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-gdi/MigraDoc.Tests-gdi.csproj +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-gdi/MigraDoc.Tests-gdi.csproj @@ -29,6 +29,7 @@ + diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-wpf/MigraDoc.Tests-wpf.csproj b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-wpf/MigraDoc.Tests-wpf.csproj index ac6a5bbd..33d21423 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-wpf/MigraDoc.Tests-wpf.csproj +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests-wpf/MigraDoc.Tests-wpf.csproj @@ -29,6 +29,7 @@ + diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/RtfRendererTests.cs b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/RtfRendererTests.cs index c6883359..9530a020 100644 --- a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/RtfRendererTests.cs +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/RtfRendererTests.cs @@ -26,9 +26,6 @@ public class RtfRendererTests [Fact] public void Create_Hello_World_RtfRendererTests() { - //// TODO Register encoding here or in RtfDocumentRenderer? - //System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); - #if CORE GlobalFontSettings.FontResolver = NewFontResolver.Get(); #endif @@ -176,8 +173,6 @@ public void Test_Tabs(bool doNotUnifyTabStopHandling) #if CORE GlobalFontSettings.FontResolver = NewFontResolver.Get(); #endif - //// TODO Register encoding here or in RtfDocumentRenderer? - //System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); var document = new Document(); @@ -589,10 +584,6 @@ public void Test_Tabs(bool doNotUnifyTabStopHandling) public void Create_Rtf_with_Image() { - //// TODO #warning empira Register this in RtfRenderer. - //// TODO Register encoding here or in RtfDocumentRenderer? - //System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); - #if CORE GlobalFontSettings.FontResolver = NewFontResolver.Get(); #endif @@ -635,10 +626,6 @@ public void Create_Rtf_with_Image() [Fact] public void Create_Rtf_with_Embedded_Base64Image() { - //// TODO #warning empira Register this in RtfRenderer. - //// TODO Register encoding here or in RtfDocumentRenderer? - //System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); - #if CORE GlobalFontSettings.FontResolver = NewFontResolver.Get(); #endif @@ -722,7 +709,6 @@ private static void AddImage(Document document) var logo = document.LastSection.AddImage(base64); } - // TODO 2023-05-23 Make tests for all three builds. #if !CORE // Not supported by Core build. [Theory] @@ -751,10 +737,6 @@ private static void AddImage(Document document) [InlineData(@"Logo landscape 256.png")] public void Create_Rtf_with_Base64Image(string assetName) { - //// TODO #warning empira Register this in RtfRenderer. - //// TODO Register encoding here or in RtfDocumentRenderer? - //System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); - #if CORE GlobalFontSettings.FontResolver = NewFontResolver.Get(); #endif @@ -805,5 +787,103 @@ public void Create_Rtf_with_Base64Image(string assetName) //// ...and start a viewer. //PdfFileHelper.StartPdfViewerIfDebugging(filename); } + + [Fact] + public void Test_Heading_Border() + { +#if CORE + GlobalFontSettings.FontResolver = NewFontResolver.Get(); +#endif + var bottomWidth = Unit.FromPoint(2.3); + var bottomColor = Colors.Blue; + var rtfBottomString = "\\brdrs\\brdrw46\\brdrcf2"; + + var headingBottomWidth = Unit.FromPoint(4.6); + var headingBottomColor = Colors.Red; + var rtfHeadingBottomString = "\\brdrs\\brdrw92\\brdrcf3"; + + var document = new Document(); + var section = document.AddSection(); + + var table = section.AddTable(); + table.Borders.Bottom.Width = bottomWidth; + table.Borders.Bottom.Color = bottomColor; + + table.AddColumn(Unit.FromCentimeter(16)); + + var headingRow = table.AddRow(); + headingRow.HeadingFormat = true; + headingRow.Cells[0].AddParagraph("Heading"); + headingRow.Borders.Bottom.Width = headingBottomWidth; + headingRow.Borders.Bottom.Color = headingBottomColor; + + // Add 4 rows with a height forcing a page break after the first two rows. + for (var rowNr = 1; rowNr <= 4; rowNr++) + { + var row = table.AddRow(); + row.Cells[0].AddParagraph($"Row {rowNr}"); + row.Height = Unit.FromCentimeter(10); + } + + var rtfFilename = PdfFileHelper.CreateTempFileName("Test_Heading_Border") + ".rtf"; + var rtfRenderer = new RtfDocumentRenderer(); + rtfRenderer.Render(document, rtfFilename, Environment.CurrentDirectory); + + + // Analyze rendered RTF. + var rtf = File.ReadAllText(rtfFilename); + + // Split by row identifier and skip the first part, which is no row. + var splitByRows = rtf.Split("\\trowd").Skip(1).ToArray(); + splitByRows.Length.Should().Be(5, "as there are 5 rows"); + + // Get the part before the padding identifier, split it by border identifier and skip the first part, which is no border. + var rowsByBorders = splitByRows + .Select(row => row.Split("\\clpad").First() + .Split("\\clbrdr").Skip(1).ToArray() + ).ToArray(); + foreach (var borders in rowsByBorders) + borders.Length.Should().Be(4, "as there are 4 borders defined"); + + + // Heading row. + var rowBorderParts = rowsByBorders[0]; + + var topBorderPart = rowBorderParts[0]; + topBorderPart.Should().StartWith("t", "first border should be top border"); + topBorderPart.Length.Should().Be(1, "heading top border should not be defined and only contain the top identifier"); + + var bottomBorderPart = rowBorderParts[3]; + bottomBorderPart.Should().StartWith("b", "last border should be bottom border"); + bottomBorderPart[1..].Should().Be(rtfHeadingBottomString, "heading bottom border should be defined heading bottom border"); + + + // Row 1. + rowBorderParts = rowsByBorders[1]; + + topBorderPart = rowBorderParts[0]; + topBorderPart.Should().StartWith("t", "first border should be top border"); + topBorderPart.Length.Should().Be(1, "row 1 top border should not be defined and only contain the top identifier, as the heading bottom border must not affect a content border," + + "to not copy heading border values when moving or inserting rows in RTF application."); + + bottomBorderPart = rowBorderParts[3]; + bottomBorderPart.Should().StartWith("b", "last border should be bottom border"); + bottomBorderPart[1..].Should().Be(rtfBottomString, "row 1 bottom border should be defined content bottom border"); + + + // Row 2-4. + for (var r = 2; r < 5; r++) + { + rowBorderParts = rowsByBorders[r]; + + topBorderPart = rowBorderParts[0]; + topBorderPart.Should().StartWith("t", "first border should be top border"); + topBorderPart[1..].Should().Be(rtfBottomString, $"row {r} top border should be the defined content bottom border of the top neighbor row"); + + bottomBorderPart = rowBorderParts[3]; + bottomBorderPart.Should().StartWith("b", "last border should be bottom border"); + bottomBorderPart[1..].Should().Be(rtfBottomString, $"row {r} bottom border should be defined content bottom border"); + } + } } } diff --git a/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/TextTests.cs b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/TextTests.cs new file mode 100644 index 00000000..7d30fae3 --- /dev/null +++ b/src/foundation/src/MigraDoc/tests/MigraDoc.Tests/TextTests.cs @@ -0,0 +1,106 @@ +// MigraDoc - Creating Documents on the Fly +// See the LICENSE file in the solution root for more information. + +using System.Diagnostics; +using PdfSharp.Pdf; +using MigraDoc.DocumentObjectModel; +using MigraDoc.DocumentObjectModel.Fields; +using MigraDoc.Rendering; +using PdfSharp.Fonts; +using PdfSharp.Snippets.Font; +using PdfSharp.TestHelper; +using Xunit; + +namespace MigraDoc.Tests +{ + [Collection("MGD")] + public class TextTests + { + [Fact] + public void Surrogate_Pairs_Test() + { +#if CORE + GlobalFontSettings.FontResolver = NewFontResolver.Get(); +#endif + + // Create a MigraDoc document. + var document = CreateDocument(); + + // ----- Unicode encoding in MigraDoc is demonstrated here. ----- + + //// A flag indicating whether to create a Unicode PDF or a WinAnsi PDF file. + //// This setting applies to all fonts used in the PDF document. + //// This setting has no effect on the RTF renderer. + //const bool unicode = false; + + // Create a renderer for the MigraDoc document. + var pdfRenderer = new PdfDocumentRenderer() + { + // Associate the MigraDoc document with a renderer. + Document = document + }; + + // Layout and render document to PDF. + pdfRenderer.RenderDocument(); + + // Save the document... + var filename = PdfFileHelper.CreateTempFileName("HelloEmoji"); + pdfRenderer.PdfDocument.Save(filename); + // ...and start a viewer. + PdfFileHelper.StartPdfViewerIfDebugging(filename); + +#if DEBUG___ + MigraDoc.DocumentObjectModel.IO.DdlWriter dw = new MigraDoc.DocumentObjectModel.IO.DdlWriter(filename + "_2.mdddl"); + dw.WriteDocument(document); + dw.Close(); +#endif + } + + /// + /// Creates an absolutely minimalistic document. + /// + static Document CreateDocument() + { + // Create a new MigraDoc document. + var document = new Document(); + + // Add a section to the document. + var section = document.AddSection(); + + // Add a paragraph to the section. + var paragraph = section.AddParagraph(); + + // Set font color. + //paragraph.Format.Font.Color = Color.FromCmyk(100, 30, 20, 50); + paragraph.Format.Font.Color = Colors.DarkBlue; + + // Add some text to the paragraph. + paragraph.AddFormattedText("Hello, World!", TextFormat.Bold); + + paragraph = section.AddParagraph("111😢😞💪"); + paragraph.Format.Font.Name = "Segoe UI Emoji"; + paragraph.AddLineBreak(); + paragraph.AddText("💩💩💩✓✔✅🐛👌🆗🖕 🦄 🦂 🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻"); + + paragraph = section.AddParagraph("111😢😞💪"); + paragraph.Format.Font.Name = "Segoe UI Emoji"; + paragraph.AddLineBreak(); + paragraph.AddText("💩💩💩✓✔✅ 🐛👌🆗🖕 🦄 🦂 🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻" + + "💩💩💩✓✔✅ 🐛👌🆗🖕 🦄 🦂 🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻" + + "💩💩💩✓✔✅ 🐛👌🆗🖕🖕🖕🖕 🦄 🦂 🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻" + + "💩💩💩✓✔✅ 🐛👌🆗🖕 🦄 🦂 🍇 🍆🍆🍆🍆🍆🍆🍆🍆🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻" + + "💩💩💩✓✔✅ 🐛👌🆗🖕 🦄 🦂🦂🦂🦂🦂🦂 🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻" + + "💩💩💩✓✔✅ 🐛👌🆗🖕 🦄 🦂 🍇🍇🍇🍇🍇🍇🍇🍇🍇🍇 🍆 ☕ 🚂 \U0001f6f8 ☁ ☢ ♌ ♏ ✅ ☑ ✔ ™ 🆒 ◻"); + + // Create the primary footer. + var footer = section.Footers.Primary; + + // Add content to footer. + paragraph = footer.AddParagraph(); + paragraph.Add(new DateField { Format = "yyyy/MM/dd HH:mm:ss" }); + paragraph.Format.Alignment = ParagraphAlignment.Center; + + return document; + } + } +} diff --git a/src/foundation/src/PDFsharp/src/PdfSharp.Charting/Charting/Point.cs b/src/foundation/src/PDFsharp/src/PdfSharp.Charting/Charting/Point.cs index 34fe60a5..adc68a39 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp.Charting/Charting/Point.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp.Charting/Charting/Point.cs @@ -21,7 +21,7 @@ public Point(double value) : this() => Value = value; /// - /// Initializes a new instance of the Point class with a real value. + /// Initializes a new instance of the Point class with a string value. /// public Point(string value) : this() { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs index 5c3d7178..f5c7c294 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Internal/ImageImporterJpeg.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the solution root for more information. using System; +using System.Text; using PdfSharp.Pdf; namespace PdfSharp.Drawing.Internal @@ -67,14 +68,9 @@ bool TestJfifHeader(StreamReaderHelper stream, ImportedImage ii) //var currentOffset = stream.CurrentOffset; bool header = TestJfifHeaderWorker(stream, ii) || - TestExifHeaderWorker(stream/*, ii*/); + TestExifHeaderWorker(stream/*, ii*/) || + TestApp13HeaderWorker(stream); - //while (!header && MoveToNextHeader(stream)) - //{ - // header = TestJfifHeaderWorker(stream, ii); - //} - - //stream.CurrentOffset = currentOffset; return header; } @@ -146,6 +142,41 @@ bool TestExifHeaderWorker(StreamReaderHelper stream/*, ImportedImage ii*/) return false; } + bool TestApp13HeaderWorker(StreamReaderHelper stream) + { + // Check for APP13 header. + if (stream.GetWord(0, true) == 0xffed) + { + int length = stream.GetWord(2, true); + + StringBuilder identifier = new(); + int idx = 4; + do + { + byte c = stream.GetByte(idx); + if (c == 0x00) + { + break; + } + identifier.Append((char)c); + ++idx; + } while (idx < length); + + var id = identifier.ToString(); + if (!id.StartsWith("Photoshop ") && !id.StartsWith("Adobe_Photoshop")) // "Photoshop 3.0", "Adobe_Photoshop2.5:", etc. + { + return false; + } + + ++idx; + if (idx + 3 < length && stream.GetDWord(idx, true) == 0x3842494d) // 8BIM + { + return true; + } + } + return false; + } + bool TestColorFormatHeader(StreamReaderHelper stream, ImportedImage ii) { // The SOS header (start of scan). diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs index 8ab7f65b..7c2f8217 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Layout/XTextFormatter.cs @@ -353,7 +353,7 @@ public Block(BlockType type) /// public bool Stop; } - // TODO: + // TODO: Possible Improvements for XTextFormatter: // - more XStringFormat variations // - calculate bounding box // - left and right indent diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs index 958cfb93..ddcebf87 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs @@ -417,7 +417,7 @@ public void RealizeFont(XFont font, XBrush brush, int renderingMode) public void AddTransform(XMatrix value, XMatrixOrder matrixOrder) { - // TODO: User matrixOrder + // TODO: Use matrixOrder #if DEBUG if (matrixOrder == XMatrixOrder.Append) throw new NotImplementedException("XMatrixOrder.Append"); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs index 7fc880d6..87153464 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/XGraphicsPdfRenderer.cs @@ -518,14 +518,27 @@ public void DrawString(string s, XFont font, XBrush brush, XRect rect, XStringFo bool isSymbolFont = descriptor.FontFace.cmap.symbol; for (int idx = 0; idx < s.Length; idx++) { + if (char.IsLowSurrogate(s, idx)) + continue; // Ignore second char of Surrogate Pair. + char ch = s[idx]; if (isSymbolFont) { // Remap ch for symbol fonts. ch = (char)(ch | (descriptor.FontFace.os2.usFirstCharIndex & 0xFF00)); // @@@ refactor } - int glyphID = descriptor.CharCodeToGlyphIndex(ch); - sb.Append((char)glyphID); + + if (char.IsHighSurrogate(ch)) + { + var glyphIdUnsigned = descriptor.CharCodeToGlyphIndex(ch, s[idx + 1]); + var glyphID = BitConverter.ToInt32(BitConverter.GetBytes(glyphIdUnsigned), 0); + sb.Append(char.ConvertFromUtf32(glyphID)); + } + else + { + var glyphID = descriptor.CharCodeToGlyphIndex(ch); + sb.Append((char)glyphID); + } } s = sb.ToString(); @@ -783,7 +796,7 @@ public void DrawImage(XImage image, XRect destRect, XRect srcRect, XGraphicsUnit } } - #endregion +#endregion // -------------------------------------------------------------------------------------------- diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/FontHelper.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/FontHelper.cs index 812960b0..661d259a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/FontHelper.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/FontHelper.cs @@ -55,13 +55,22 @@ public static XSize MeasureString(string text, XFont font, XStringFormat stringF if (ch < 32) continue; + if (char.IsLowSurrogate(ch)) + continue; // Don't process high surrogate. Low will process this char. + if (symbol) { // Remap ch for symbol fonts. ch = (char)(ch | (descriptor.FontFace.os2.usFirstCharIndex & 0xFF00)); // @@@ refactor // Used | instead of + because of: http://pdfsharp.codeplex.com/workitem/15954 } - int glyphIndex = descriptor.CharCodeToGlyphIndex(ch); + + uint glyphIndex; + if (char.IsHighSurrogate(ch)) + glyphIndex = descriptor.CharCodeToGlyphIndex(ch, text[idx + 1]); + else + glyphIndex = descriptor.CharCodeToGlyphIndex(ch); + width += descriptor.GlyphIndexToWidth(glyphIndex); } // What? size.Width = width * font.Size * (font.Italic ? 1 : 1) / descriptor.UnitsPerEm; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs index f34ec687..c38d1dbf 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XFont.cs @@ -462,7 +462,7 @@ void CreateDescriptorAndInitializeFontMetrics() // TODO: refactor [Browsable(false)] public XFontFamily FontFamily => GlyphTypeface.FontFamily; - // TODO + // TODO XFont.Name /// /// WRONG: Gets the face name of this Font object. /// Indeed, it returns the font family name. diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs index 49cecbdf..ecd338b1 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphics.cs @@ -73,7 +73,7 @@ public sealed class XGraphics : IDisposable /// /// Initializes a new instance of the XGraphics class. /// - /// The gfx. + /// The gfx. Can be null for a measuring context. /// The size. /// The page unit. /// The page direction. @@ -4348,7 +4348,9 @@ public XGraphicsContainer BeginContainer(XRect dstrect, XRect srcrect, XGraphics try { Lock.EnterGdiPlus(); - xContainer = new XGraphicsContainer(_gfx != null! ? _gfx.Save() : null); + if (_gfx is null) + throw new InvalidOperationException(nameof(_gfx)); + xContainer = new XGraphicsContainer(_gfx.Save()); } finally { Lock.ExitGdiPlus(); } } @@ -4392,12 +4394,12 @@ public void EndContainer(XGraphicsContainer container) // Nothing to do. #endif #if GDI - if (TargetContext == XGraphicTargetContext.GDI && _gfx != null) + if (TargetContext == XGraphicTargetContext.GDI /*&& _gfx != null*/) // NRT { try { Lock.EnterGdiPlus(); - _gfx.Restore(container.GdiState!); // BUG NRT + _gfx.Restore(container.GdiState); } finally { Lock.ExitGdiPlus(); } } @@ -5078,6 +5080,7 @@ internal XImage? AssociatedImage #if GDI /// /// Always defined System.Drawing.Graphics object. Used as 'query context' for PDF pages. + /// Can be null for measuring contexts, but those are not used for drawing. /// internal Graphics _gfx; #endif diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphicsContainer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphicsContainer.cs index 42168aaf..0b57dbe2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphicsContainer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XGraphicsContainer.cs @@ -17,11 +17,11 @@ namespace PdfSharp.Drawing public sealed class XGraphicsContainer { #if GDI - internal XGraphicsContainer(GraphicsState? state) + internal XGraphicsContainer(GraphicsState state) { GdiState = state; } - internal GraphicsState? GdiState; + internal GraphicsState GdiState; #endif #if WPF internal XGraphicsContainer() diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XImage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XImage.cs index 6a0071cc..cf457e8f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XImage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing/XImage.cs @@ -408,7 +408,7 @@ internal static XImage FromImportedImage(ImportedImage image) /// The path to a BMP, PNG, GIF, JPEG, TIFF, or PDF file. public static bool ExistsFile(string path) { - // Support for "base64:" pseudo protocol is a MigraDoc feature, currently completely implemented in MigraDoc files. TODO: Does support for "base64:" make sense for PDFsharp? Probably not as PDFsharp can handle images from streams. + // Support for "base64:" pseudo protocol is a MigraDoc feature, currently completely implemented in MigraDoc files. //if (path.StartsWith("base64:", StringComparison.Ordinal)) // The Image is stored in the string here, so the file exists. // return true; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/FontDescriptor.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/FontDescriptor.cs index ba18f76f..04da1123 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/FontDescriptor.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/FontDescriptor.cs @@ -269,7 +269,7 @@ internal static string ComputeKey(string name, bool isBold, bool isItalic) else /*if (isBold && isItalic)*/ return name.ToLowerInvariant() + "/bi"; #else - // TODO StringBuilder? + // TODO Is StringBuilder more efficient? string key = name.ToLowerInvariant() + '/' + (isBold ? "b" : "") + (isItalic ? "i" : ""); return key; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/GlyphDataTable.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/GlyphDataTable.cs index cb197e7f..78ce3ab5 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/GlyphDataTable.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/GlyphDataTable.cs @@ -56,7 +56,7 @@ public void Read() /// /// Gets the data of the specified glyph. /// - public byte[] GetGlyphData(int glyph) + public byte[] GetGlyphData(uint glyph) { //var loca = _fontData!.loca; int start = GetOffset(glyph); @@ -70,7 +70,7 @@ public byte[] GetGlyphData(int glyph) /// /// Gets the size of the byte array that defines the glyph. /// - public int GetGlyphSize(int glyph) + public int GetGlyphSize(uint glyph) { //var loca = _fontData.loca; return GetOffset(glyph + 1) - GetOffset(glyph); @@ -79,7 +79,7 @@ public int GetGlyphSize(int glyph) /// /// Gets the offset of the specified glyph relative to the first byte of the font image. /// - public int GetOffset(int glyph) + public int GetOffset(uint glyph) { return DirectoryEntry.Offset + _fontData!.loca.LocaTable[glyph]; } @@ -87,10 +87,10 @@ public int GetOffset(int glyph) /// /// Adds for all composite glyphs the glyphs the composite one is made of. /// - public void CompleteGlyphClosure(Dictionary glyphs) + public void CompleteGlyphClosure(Dictionary glyphs) { int count = glyphs.Count; - int[] glyphArray = new int[glyphs.Count]; + uint[] glyphArray = new uint[glyphs.Count]; glyphs.Keys.CopyTo(glyphArray, 0); if (!glyphs.ContainsKey(0)) glyphs.Add(0, null); @@ -107,7 +107,7 @@ public void CompleteGlyphClosure(Dictionary glyphs) /// /// If the specified glyph is a composite glyph add the glyphs it is made of to the glyph table. /// - void AddCompositeGlyphs(Dictionary glyphs, int glyph) + void AddCompositeGlyphs(Dictionary glyphs, uint glyph) { //int start = fontData.loca.GetOffset(glyph); int start = GetOffset(glyph); @@ -123,7 +123,7 @@ void AddCompositeGlyphs(Dictionary glyphs, int glyph) for (; ; ) { int flags = _fontData.ReadUFWord(); - int cGlyph = _fontData.ReadUFWord(); + uint cGlyph = _fontData.ReadUFWord(); if (!glyphs.ContainsKey(cGlyph)) glyphs.Add(cGlyph, null); if ((flags & MORE_COMPONENTS) == 0) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeDescriptor.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeDescriptor.cs index d29ca9c5..d8c48d89 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeDescriptor.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeDescriptor.cs @@ -239,7 +239,7 @@ void Initialize() // Remap ch for symbol fonts. ch = (char)(ch | (FontFace.os2.usFirstCharIndex & 0xFF00)); // @@@ refactor } - int glyphIndex = CharCodeToGlyphIndex(ch); + var glyphIndex = CharCodeToGlyphIndex(ch); Widths[idx] = GlyphIndexToPdfWidth(glyphIndex); } } @@ -265,7 +265,7 @@ internal int DesignUnitsToPdf(double value) /// See OpenType spec "cmap - Character To Glyph Index Mapping Table / Format 4: Segment mapping to delta values" /// for details about this a little bit strange looking algorithm. /// - public int CharCodeToGlyphIndex(char value) + public uint CharCodeToGlyphIndex(char value) { //try //{ @@ -284,7 +284,7 @@ public int CharCodeToGlyphIndex(char value) return 0; if (cmap.idRangeOffs[seg] == 0) - return (value + cmap.idDelta[seg]) & 0xFFFF; + return (value + (uint)cmap.idDelta[seg]) & 0xFFFF; int idx = cmap.idRangeOffs[seg] / 2 + (value - cmap.startCount[seg]) - (segCount - seg); Debug.Assert(idx >= 0 && idx < cmap.glyphCount); @@ -292,7 +292,7 @@ public int CharCodeToGlyphIndex(char value) if (cmap.glyphIdArray[idx] == 0) return 0; - return (cmap.glyphIdArray[idx] + cmap.idDelta[seg]) & 0xFFFF; + return (cmap.glyphIdArray[idx] + (uint)cmap.idDelta[seg]) & 0xFFFF; //} //catch @@ -302,19 +302,53 @@ public int CharCodeToGlyphIndex(char value) //} } + /// + /// Maps a Unicode to the index of the corresponding glyph. + /// See OpenType spec "cmap - Character To Glyph Index Mapping Table / Format 4: Segment mapping to delta values" + /// for details about this a little bit strange looking algorithm. + /// + public uint CharCodeToGlyphIndex(char highSurrogate, char lowSurrogate) + { + try + { + var value = char.ConvertToUtf32(highSurrogate, lowSurrogate); + + var converted = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + var cmap = FontFace.cmap.cmap12; + + int seg; + for (seg = 0; seg < cmap.groups.Length; seg++) + { + if (value <= cmap.groups[seg].endCharCode) + break; + } + Debug.Assert(seg < cmap.groups.Length); + + if (value < cmap.groups[seg].startCharCode) + return 0; + + return cmap.groups[seg].startGlyphID + converted - cmap.groups[seg].startCharCode; + } + catch + { + GetType(); + throw; + } + } + /// /// Converts the width of a glyph identified by its index to PDF design units. /// - public int GlyphIndexToPdfWidth(int glyphIndex) + public int GlyphIndexToPdfWidth(uint glyphIndex) { try { - int numberOfHMetrics = FontFace.hhea.numberOfHMetrics; - int unitsPerEm = FontFace!.head!.unitsPerEm; + var numberOfHMetrics = FontFace.hhea.numberOfHMetrics; + var unitsPerEm = FontFace!.head!.unitsPerEm; // glyphIndex >= numberOfHMetrics means the font is mono-spaced and all glyphs have the same width if (glyphIndex >= numberOfHMetrics) - glyphIndex = numberOfHMetrics - 1; + glyphIndex = numberOfHMetrics - (uint)1; int width = FontFace.hmtx.Metrics[glyphIndex].advanceWidth; @@ -332,7 +366,7 @@ public int GlyphIndexToPdfWidth(int glyphIndex) public int PdfWidthFromCharCode(char ch) { - int idx = CharCodeToGlyphIndex(ch); + var idx = CharCodeToGlyphIndex(ch); int width = GlyphIndexToPdfWidth(idx); return width; } @@ -340,11 +374,11 @@ public int PdfWidthFromCharCode(char ch) /// /// Converts the width of a glyph identified by its index to PDF design units. /// - public double GlyphIndexToEmfWidth(int glyphIndex, double emSize) + public double GlyphIndexToEmfWidth(uint glyphIndex, double emSize) { try { - int numberOfHMetrics = FontFace.hhea.numberOfHMetrics; + uint numberOfHMetrics = FontFace.hhea.numberOfHMetrics; int unitsPerEm = FontFace!.head!.unitsPerEm; // glyphIndex >= numberOfHMetrics means the font is mono-spaced and all glyphs have the same width @@ -365,11 +399,11 @@ public double GlyphIndexToEmfWidth(int glyphIndex, double emSize) /// /// Converts the width of a glyph identified by its index to PDF design units. /// - public int GlyphIndexToWidth(int glyphIndex) + public int GlyphIndexToWidth(uint glyphIndex) { try { - int numberOfHMetrics = FontFace.hhea.numberOfHMetrics; + uint numberOfHMetrics = FontFace.hhea.numberOfHMetrics; // glyphIndex >= numberOfHMetrics means the font is mono-spaced and all glyphs have the same width if (glyphIndex >= numberOfHMetrics) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs index bd0d348e..bed7d529 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontTables.cs @@ -23,7 +23,18 @@ enum PlatformId /// enum WinEncodingId { +#if true + Symbol = 0, + UnicodeUSC_2 = 1, + //ShiftJIS = 2, + //PRC = 3, + //Big5 = 4, + //Wansung = 5, + //Johab = 6, + UnicodeUSC_4 = 10 +#else Symbol, Unicode +#endif } /// @@ -107,6 +118,63 @@ internal void Read() } } + /// + /// CMap format 12: Segmented coverage. + /// The Windows standard format. + /// + internal class CMap12 : OpenTypeFontTable + { + internal struct SequentialMapGroup + { + public UInt32 startCharCode;// First character code in this group. + public UInt32 endCharCode; // Last character code in this group. + public UInt32 startGlyphID; // Glyph index corresponding to the starting character code. + } + + public WinEncodingId encodingId; // Windows encoding ID. + public UInt16 format; // Subtable format; set to 12. + public UInt32 length; // Byte length of this subtable (including the header). + public UInt32 language; // This field must be set to zero for all cmap subtables whose platform IDs are other than Macintosh (platform ID 1). + public UInt32 numGroups; // Number of groupings which follow. + + public SequentialMapGroup[] groups = null!; + + public CMap12(OpenTypeFontface fontData, WinEncodingId encodingId) + : base(fontData, "----") + { + this.encodingId = encodingId; + Read(); + } + + internal void Read() + { + try + { + // m_EncodingID = encID; + format = _fontData!.ReadUShort(); // NRT + Debug.Assert(format == 12, "Only format 12 expected."); + _fontData.ReadUShort(); // Reserved. + length = _fontData.ReadULong(); + language = _fontData.ReadULong(); // Always null in Windows. + numGroups = _fontData.ReadULong(); + + groups = new SequentialMapGroup[numGroups]; + + for (int i = 0; i < groups.Length; i++) + { + ref var group = ref groups[i]; + group.startCharCode = _fontData.ReadULong(); + group.endCharCode = _fontData.ReadULong(); + group.startGlyphID = _fontData.ReadULong(); + } + } + catch (Exception ex) + { + throw new InvalidOperationException(PSSR.ErrorReadingFontData, ex); + } + } + } + /// /// This table defines the mapping of character codes to the glyph index values used in the font. /// It may contain more than one subtable, in order to support more than one character encoding scheme. @@ -124,6 +192,7 @@ class CMapTable : OpenTypeFontTable public bool symbol; public CMap4 cmap4 = null!; + public CMap12 cmap12 = null!; /// /// Initializes a new instance of the class. @@ -142,10 +211,6 @@ internal void Read() version = _fontData.ReadUShort(); numTables = _fontData.ReadUShort(); -#if DEBUG_ - if (_fontData.Name == "Cambria") - Debug-Break.Break(); -#endif bool success = false; for (int idx = 0; idx < numTables; idx++) @@ -157,16 +222,32 @@ internal void Read() int currentPosition = _fontData.Position; // Just read Windows stuff. - if (platformId == PlatformId.Win && (encodingId == WinEncodingId.Symbol || encodingId == WinEncodingId.Unicode)) + if (platformId == PlatformId.Win && + (encodingId == WinEncodingId.Symbol || + encodingId == WinEncodingId.UnicodeUSC_2 || + encodingId == WinEncodingId.UnicodeUSC_4)) { symbol = encodingId == WinEncodingId.Symbol; _fontData.Position = tableOffset + offset; - cmap4 = new CMap4(_fontData, encodingId); + + var format = _fontData.ReadUShort(); + _fontData.Position = tableOffset + offset; + + if (format == 4) + { + cmap4 = new(_fontData, encodingId); + } + else if (format == 12) + { + cmap12 = new(_fontData, encodingId); + } + _fontData.Position = currentPosition; - // We have found what we are looking for, so break. + + // We have found what we are looking for, but we do not break as there may be another hit. success = true; - break; + // break; } } if (!success) @@ -477,7 +558,7 @@ class VerticalMetricsTable : OpenTypeFontTable // UNDONE public const string Tag = TableTagNames.VMtx; - // code comes from HorizontalMetricsTable + // Code comes from HorizontalMetricsTable. public HorizontalMetrics[] metrics; public FWord[] leftSideBearing; @@ -504,7 +585,7 @@ public void Read() metrics = new HorizontalMetrics[numMetrics]; for (int idx = 0; idx < numMetrics; idx++) - metrics[idx] = new HorizontalMetrics(_fontData); + metrics[idx] = new(_fontData); if (numLsbs > 0) { @@ -889,7 +970,7 @@ public void Read() /// /// This table contains a list of values that can be referenced by instructions. /// They can be used, among other things, to control characteristics for different glyphs. - /// The length of the table must be an integral number of FWORD units. + /// The length of the table must be an integral number of FWORD units. /// class ControlValueTable : OpenTypeFontTable { @@ -924,7 +1005,7 @@ public void Read() /// /// This table is similar to the CVT Program, except that it is only run once, when the font is first used. /// It is used only for FDEFs and IDEFs. Thus the CVT Program need not contain function definitions. - /// However, the CVT Program may redefine existing FDEFs or IDEFs. + /// However, the CVT Program may redefine existing FDEFs or IDEFs. /// class FontProgram : OpenTypeFontTable { @@ -957,10 +1038,10 @@ public void Read() } /// - /// The Control Value Program consists of a set of TrueType instructions that will be executed whenever the font or + /// The Control Value Program consists of a set of TrueType instructions that will be executed whenever the font or /// point size or transformation matrix change and before each glyph is interpreted. Any instruction is legal in the /// CVT Program but since no glyph is associated with it, instructions intended to move points within a particular - /// glyph outline cannot be used in the CVT Program. The name 'prep' is anachronistic. + /// glyph outline cannot be used in the CVT Program. The name 'prep' is anachronistic. /// class ControlValueProgram : OpenTypeFontTable { @@ -983,7 +1064,7 @@ public void Read() int length = DirectoryEntry.Length; bytes = new byte[length]; for (int idx = 0; idx < length; idx++) - bytes[idx] = _fontData!.ReadByte(); // NRT + bytes[idx] = _fontData!.ReadByte(); // NRT } catch (Exception ex) { @@ -994,7 +1075,7 @@ public void Read() /// /// This table contains information that describes the glyphs in the font in the TrueType outline format. - /// Information regarding the rasterizer (scaler) refers to the TrueType rasterizer. + /// Information regarding the rasterizer (scaler) refers to the TrueType rasterizer. /// class GlyphSubstitutionTable : OpenTypeFontTable { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs index 093d1717..8e6157fd 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts.OpenType/OpenTypeFontface.cs @@ -359,7 +359,7 @@ internal void Read() /// /// Creates a new font image that is a subset of this font image containing only the specified glyphs. /// - public OpenTypeFontface CreateFontSubSet(Dictionary glyphs, bool cidFont) + public OpenTypeFontface CreateFontSubSet(Dictionary glyphs, bool cidFont) { // Create new font image var fontData = new OpenTypeFontface(this); @@ -394,7 +394,7 @@ public OpenTypeFontface CreateFontSubSet(Dictionary glyphs, bool ci // Create a sorted array of all used glyphs. int glyphCount = glyphs.Count; - int[] glyphArray = new int[glyphCount]; + uint[] glyphArray = new uint[glyphCount]; glyphs.Keys.CopyTo(glyphArray, 0); Array.Sort(glyphArray); @@ -414,7 +414,7 @@ public OpenTypeFontface CreateFontSubSet(Dictionary glyphs, bool ci // Fill new glyf and loca table. int glyphOffset = 0; int glyphIndex = 0; - for (int idx = 0; idx < numGlyphs; idx++) + for (uint idx = 0; idx < numGlyphs; idx++) { locaNew.LocaTable[idx] = glyphOffset; if (glyphIndex < glyphCount && glyphArray[glyphIndex] == idx) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/CMapInfo.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/CMapInfo.cs index 6681cb45..baa39653 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/CMapInfo.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Fonts/CMapInfo.cs @@ -32,8 +32,11 @@ public void AddChars(string text) int length = text.Length; for (int idx = 0; idx < length; idx++) { + if (char.IsLowSurrogate(text, idx)) + continue; // Ignore the second char of a surrogate pair. + char ch = text[idx]; - if (!CharacterToGlyphIndex.ContainsKey(ch)) + if (!CharacterToGlyphIndex.ContainsKey(ch) || char.IsHighSurrogate(ch)) { char ch2 = ch; if (symbol) @@ -41,8 +44,27 @@ public void AddChars(string text) // Remap ch for symbol fonts. ch2 = (char)(ch | (_descriptor.FontFace.os2.usFirstCharIndex & 0xFF00)); // @@@ refactor } - int glyphIndex = _descriptor.CharCodeToGlyphIndex(ch2); - CharacterToGlyphIndex.Add(ch, glyphIndex); + uint glyphIndex; + + if (char.IsHighSurrogate(ch)) + { + // If high surrogate char hasn't been added yet, add high and low surrogate chars: + if (!SurrogatePairs.ContainsKey(ch)) + SurrogatePairs.Add(ch, new List(text[idx + 1])); + // If high surrogate char has been added and low surrogate char hasn't been added yet, add low surrogate char: + else if (SurrogatePairs.ContainsKey(ch) && !SurrogatePairs[ch].Contains(text[idx + 1])) + SurrogatePairs[ch].Add(text[idx + 1]); + // If high and low surrogate chars have been added, continue with next loop: + else + continue; + glyphIndex = _descriptor.CharCodeToGlyphIndex(ch, text[idx + 1]); + } + else + glyphIndex = _descriptor.CharCodeToGlyphIndex(ch2); + + if (!CharacterToGlyphIndex.ContainsKey(ch)) // To do (for support of reading PDF?): Surrogate pair chars with same high surrogate chars and different low surrogate chars are missing in "CharacterToGlyphIndex"! + CharacterToGlyphIndex.Add(ch, glyphIndex); + GlyphIndices[glyphIndex] = default!; MinChar = (char)Math.Min(MinChar, ch); MaxChar = (char)Math.Max(MaxChar, ch); @@ -61,7 +83,7 @@ public void AddGlyphIndices(string glyphIndices) int length = glyphIndices.Length; for (int idx = 0; idx < length; idx++) { - int glyphIndex = glyphIndices[idx]; + var glyphIndex = glyphIndices[idx]; GlyphIndices[glyphIndex] = null!; } } @@ -99,9 +121,9 @@ public char[] Chars } } - public int[] GetGlyphIndices() + public uint[] GetGlyphIndices() { - int[] indices = new int[GlyphIndices.Count]; + uint[] indices = new uint[GlyphIndices.Count]; GlyphIndices.Keys.CopyTo(indices, 0); Array.Sort(indices); return indices; @@ -109,7 +131,8 @@ public int[] GetGlyphIndices() public char MinChar = Char.MaxValue; public char MaxChar = Char.MinValue; - public Dictionary CharacterToGlyphIndex = new Dictionary(); - public Dictionary GlyphIndices = new Dictionary(); + public Dictionary CharacterToGlyphIndex = new Dictionary(); + public Dictionary GlyphIndices = new Dictionary(); + private Dictionary> SurrogatePairs = new Dictionary>(); } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.FaxEncode.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.FaxEncode.cs index 55fe3560..7cbb3a2d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.FaxEncode.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.FaxEncode.cs @@ -722,7 +722,7 @@ internal void WriteBits(uint value, uint bits) #endif } - // We only come here if bits fits. + // We only come here if bits fit. _buffer = (_buffer << (int)bits) + (value & masks[bits]); _bitsInBuffer += bits; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.cs index fa8494f3..590292c3 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfImage.cs @@ -1046,11 +1046,13 @@ void ReadIndexedMemoryBitmap(int bits) { throw new NotImplementedException("ReadIndexedMemoryBitmap: unsupported format"); } -#if WPF - // TODOWPF: bug with height and width - width = ReadDWord(imageBits, 18); - height = ReadDWord(imageBits, 22); -#endif + +//#if WPF +// // These two lines should be superfluous, otherwise an exception would have been thrown above. +// width = ReadDWord(imageBits, 18); +// height = ReadDWord(imageBits, 22); +//#endif + int fileBits = ReadWord(imageBits, 28); if (fileBits != bits) { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs index 1404af05..a9353a6f 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfReference.cs @@ -188,7 +188,7 @@ public PdfDocument Document #if DEBUG if (_document == null) { - LogHost.Logger.LogDebug($"Document of object {_objectID} is null."); + LogHost.Logger.LogDebug("Document of object {_objectID} is null.", _objectID); } #endif return _document!; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfResources.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfResources.cs index c4e7030f..203c5b4e 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfResources.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfResources.cs @@ -276,7 +276,7 @@ string NextShadingName /// /// Check whether a resource name is already used in the context of this resource dictionary. - /// PDF4NET uses GUIDs as resource names, but I think this weapon is to heavy. + /// PDF4NET uses GUIDs as resource names, but I think this weapon is too heavy. /// internal bool ExistsResourceName(string name) { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfToUnicodeMap.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfToUnicodeMap.cs index 6496c657..bff57ea8 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfToUnicodeMap.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfToUnicodeMap.cs @@ -54,7 +54,7 @@ internal override void PrepareForSave() Dictionary glyphIndexToCharacter = new Dictionary(); int lowIndex = 65536, hiIndex = -1; - foreach (KeyValuePair entry in _cmapInfo.CharacterToGlyphIndex) + foreach (KeyValuePair entry in _cmapInfo.CharacterToGlyphIndex) { int index = (int)entry.Value; lowIndex = Math.Min(lowIndex, index); diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs index 9c67c186..99c3c23d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfType0Font.cs @@ -111,7 +111,7 @@ internal override void PrepareForSave() StringBuilder w = new StringBuilder("["); if (_cmapInfo != null) { - int[] glyphIndices = _cmapInfo.GetGlyphIndices(); + uint[] glyphIndices = _cmapInfo.GetGlyphIndices(); int count = glyphIndices.Length; int[] glyphWidths = new int[count]; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs index 815c37a7..cec2e354 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Lexer.cs @@ -554,7 +554,7 @@ public Symbol ScanLiteralString() ch = Chars.BackSlash; break; - // AutoCAD PDFs my contain such strings: (\ ) + // AutoCAD PDFs may contain such strings: (\ ) case ' ': ch = ' '; break; diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs index 8822ba17..119deb75 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/Parser.cs @@ -107,14 +107,14 @@ public PdfObject ReadObject(PdfObject? pdfObject, PdfObjectID objectID, bool inc // This only happens with corrupt PDF files that have duplicate IDs. if (iref.Value != null!) { - LogHost.Logger.LogWarning($"Another instance of object {iref} was found. Using previously encountered object instead."); + LogHost.Logger.LogWarning("Another instance of object {iref} was found. Using previously encountered object instead.", iref); // Attempt to read an object that was already read. Keep the former object. return iref.Value; } if (iref.Position >= 0) { - LogHost.Logger.LogWarning($"Another instance of object {iref} was found. Keeping reference to previously encountered object."); + LogHost.Logger.LogWarning("Another instance of object {iref} was found. Keeping reference to previously encountered object.", iref); // The object ID was already found, but the object was not read yet. // We ignore the object in the object stream and return a dummy object. // Better: Do not call this method in the first place. @@ -1285,8 +1285,10 @@ PdfTrailer ReadXRefStream(PdfCrossReferenceTable xrefTable) Debug.Assert(w!.Elements.Count == 3); int[] wsize = { w.Elements.GetInteger(0), w.Elements.GetInteger(1), w.Elements.GetInteger(2) }; int wsum = StreamHelper.WSize(wsize); +#if DEBUG if (wsum * subsectionEntryCount != bytes.Length) GetType(); +#endif // BUG: This assertion fails with original PDF 2.0 documentation (ISO_32000-2_2020(en).pdf) //Debug.Assert(wsum * subsectionEntryCount == bytes.Length, "Check implementation here."); #if DEBUG_ && CORE diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/ShiftStack.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/ShiftStack.cs index 782d9c05..74615146 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/ShiftStack.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.IO/ShiftStack.cs @@ -81,7 +81,7 @@ public void Shift(PdfItem item) } /// - /// Replaces the last 'count' items with the specified item. + /// Removes the last 'count' items. /// public void Reduce(int count) { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs index c6de2577..c5054814 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfDocument.cs @@ -188,9 +188,9 @@ static string NewName() //internal bool CanModify => true; internal bool CanModify => _openMode == PdfDocumentOpenMode.Modify; - // TODO Explain what Close() actually does. /// /// Closes this instance. + /// Saves the document if the PdfDocument was created with a filename or a stream. /// public void Close() { diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfInteger.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfInteger.cs index 1b820b9d..cde7e0ed 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfInteger.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfInteger.cs @@ -58,7 +58,7 @@ double IConvertible.ToDouble(IFormatProvider? provider) DateTime IConvertible.ToDateTime(IFormatProvider? provider) { - // TODO: Add PdfInteger.ToDateTime implementation + // TODO: Add PdfInteger.ToDateTime implementation return new DateTime(); } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs index ca0f4755..98efe600 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfObject.cs @@ -391,7 +391,7 @@ static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject if (item is PdfReference iref) { // Case: The item is a reference. - // Does the iref already belongs to the new owner? + // Does the iref already belong to the new owner? if (iref.Document == owner) { // Yes: fine. Happens when an already cloned object is reused. @@ -546,7 +546,7 @@ public PdfReference? Reference /// Gets the indirect reference of this object. Throws if it is null. /// /// The indirect reference must be not null here. - public PdfReference ReferenceNotNull // TODO: Name in need of improvement. + public PdfReference ReferenceNotNull // TODO: Name in need of improvement. => _iref ?? throw new InvalidOperationException("The indirect reference must be not null here."); } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs index 9b6767e9..2297b35d 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf/PdfUInteger.cs @@ -142,7 +142,7 @@ public char ToChar(IFormatProvider? provider) /// public object ToType(Type conversionType, IFormatProvider? provider) { - // TODO: Add PdfUInteger.ToType implementation + // TODO: Add PdfUInteger.ToType implementation //return null!; throw new NotImplementedException(nameof(ToType)); } diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/ImageTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/ImageTests.cs index f90879db..5ea0edc3 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/ImageTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/ImageTests.cs @@ -74,7 +74,7 @@ public void PDF_with_Images() //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\BlackwhiteA.bmp"; // OK //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\BlackwhiteA2.bmp"; // OK - var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\BlackwhiteTXT.bmp"; // OK + //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\BlackwhiteTXT.bmp"; // OK //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\Color4A.bmp"; // OK //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\Color8A.bmp"; // OK //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\bmp\GrayscaleA.bmp"; // OK @@ -101,6 +101,8 @@ public void PDF_with_Images() //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\Logo landscape.png"; // RGB24 //var imagePath = @"..\..\..\..\..\..\..\..\..\assets\PDFsharp\images\samples\Logo landscape 256.png"; // Palette8 + var imagePath = @"..\..\..\..\..\..\..\..\..\assets\pdfsharp-6.x\images\jpeg\extern\Zoo_JPEG_8BIM.jpg"; // OK + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) imagePath = imagePath.Replace('\\', '/'); diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/TextTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/TextTests.cs new file mode 100644 index 00000000..7754bff4 --- /dev/null +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/TextTests.cs @@ -0,0 +1,54 @@ +// PDFsharp - A .NET library for processing PDF +// See the LICENSE file in the solution root for more information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using PdfSharp.Drawing; +using PdfSharp.Fonts; +using PdfSharp.Pdf; +using PdfSharp.Pdf.Advanced; +using PdfSharp.Pdf.Content; +using PdfSharp.Pdf.Filters; +using PdfSharp.Pdf.IO; +using PdfSharp.Snippets.Font; +using PdfSharp.TestHelper; +using Xunit; + +namespace PdfSharp.Tests +{ + public class TextTests + { + [Fact/*(Skip = "Not working in Core build")*/] + public void PDF_with_Emojis() + { + GlobalFontSettings.FontResolver ??= NewFontResolver.Get(); + + // Create a new PDF document. + var document = new PdfDocument(); + document.Info.Title = "Created with PDFsharp"; + document.Info.Author = "111😢😞💪"; + document.Info.Subject = "111😢😞💪"; + + // Create an empty page in this document. + var page = document.AddPage(); + + // Get an XGraphics object for drawing on this page. + var gfx = XGraphics.FromPdfPage(page); + + XPdfFontOptions options = new XPdfFontOptions(PdfFontEncoding.Unicode); + XFont font = new XFont("Segoe UI Emoji", 12, XFontStyleEx.Regular, options); + gfx.DrawString("111😢😞💪", font, XBrushes.Black, new XRect(0, 0, page.Width, page.Height), XStringFormats.Center); + gfx.DrawString("\ud83d\udca9\ud83d\udca9\ud83d\udca9\u2713\u2714\u2705\ud83d\udc1b\ud83d\udc4c\ud83c\udd97\ud83d\udd95 \ud83e\udd84 \ud83e\udd82 \ud83c\udf47 \ud83c\udf46 \u2615 \ud83d\ude82 \ud83d\udef8 \u2601 \u2622 \u264c \u264f \u2705 \u2611 \u2714 \u2122 \ud83c\udd92 \u25fb", font, XBrushes.Black, new XRect(0, 50, page.Width, page.Height), XStringFormats.Center); + + // Save the document... + string filename = PdfFileHelper.CreateTempFileName("HelloEmoji"); + document.Save(filename); + // ...and start a viewer. + PdfFileHelper.StartPdfViewerIfDebugging(filename); + } + } +} diff --git a/src/foundation/src/shared/src/PdfSharp.Quality/Feature.cs b/src/foundation/src/shared/src/PdfSharp.Quality/Feature.cs index 4a1015ce..46c07875 100644 --- a/src/foundation/src/shared/src/PdfSharp.Quality/Feature.cs +++ b/src/foundation/src/shared/src/PdfSharp.Quality/Feature.cs @@ -119,7 +119,7 @@ protected string ReadWritePdfDocument(string filename, PdfPasswordProvider? pass } catch (Exception ex) { - LogHost.Logger.LogError(ex, $"{nameof(ReadWritePdfDocument)} failed with file '{filename}'."); + LogHost.Logger.LogError(ex, $"{nameof(ReadWritePdfDocument)} failed with file '{{filename}}'.", filename); throw; } return outFilename; diff --git a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/NewFontResolver.cs b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/NewFontResolver.cs index 322aa483..6581967e 100644 --- a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/NewFontResolver.cs +++ b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/NewFontResolver.cs @@ -72,6 +72,8 @@ static NewFontResolver() new("Lucida Console", "lucon", "LucidaConsole", "DejaVu Sans Mono"), + new("Segoe UI Emoji", "seguiemj"), // No Linux substitute + new("Symbol", "symbol", "", "Noto Sans Symbols Regular"), // Noto Symbols may not replace exactly new("Wingdings", "wingding"), // No Linux substitute diff --git a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/SegoeUIFontResolver.cs b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/SegoeUIFontResolver.cs index 35f96674..1002c3fd 100644 --- a/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/SegoeUIFontResolver.cs +++ b/src/foundation/src/shared/src/PdfSharp.Snippets/Font/fontresolving/SegoeUIFontResolver.cs @@ -11,7 +11,7 @@ namespace PdfSharp.Snippets.Font // BUG HACK /// - /// Maps font requests for a SegoeWP font to a bunch of 6 specific font files. These 6 fonts are embedded as resources in the WPFonts assembly. + /// Maps font requests for a Segoe WP, Segoe UI, or any other font to a bunch of 6 specific font files. These 6 fonts are embedded as resources in the WPFonts assembly. /// public class SegoeUiFontResolver : IFontResolver { diff --git a/src/samples/src/PDFsharp/src/HelloWorld/Program.cs b/src/samples/src/PDFsharp/src/HelloWorld/Program.cs index 0b506509..0c20e178 100644 --- a/src/samples/src/PDFsharp/src/HelloWorld/Program.cs +++ b/src/samples/src/PDFsharp/src/HelloWorld/Program.cs @@ -60,14 +60,6 @@ static void Main(string[] args) gfx.DrawString(GitVersionInformation.BranchName, font, XBrushes.Black, new XRect(0, 50, page.Width, page.Height), XStringFormats.Center); -#if true_ - // Draw empty path. - { - XGraphicsPath path = new XGraphicsPath(); - gfx.DrawPath(XBrushes.Black, path); - } -#endif - // Save the document... //const string filename = "HelloWorld.pdf"; var dir = System.IO.Directory.GetCurrentDirectory();