From 83ec240254d68529a946c4c6c83df248e5bb7811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 22 Sep 2026 11:12:43 +0200 Subject: [PATCH 01/17] started work in scaling pdf with percentage --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 10 ++++++++++ .../Settings/PdfPageSettings.cs | 16 ++++++++++++++++ src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs | 12 ++++++------ .../Export/PdfExport/Settings/GetPdfSettings.cs | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 488498dca..c6e49ad1d 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -1127,5 +1127,15 @@ public void PngTransparentTest() string path = _pdfPath + "Allsvenskan2001.pdf"; ws.SaveAsPdf(path); } + + [TestMethod] + public void ScalingTests() + { + using var p = OpenTemplatePackage("PDFTest.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.Scale = 50; + string path = _pdfPath + "ScalingTest1.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index c951d559c..5a747d226 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -196,6 +196,22 @@ public PdfScaling Scaling } } + internal double ContentScale + { + get + { + if (_scaling != null && + _scaling.ScalingMode == ScalingMode.AdjustToNormalSize && + _scaling.Scale > 0d) + { + return _scaling.Scale; + } + return 1d; + } + } + internal double EffectiveContentWidth => ContentBounds.Width / ContentScale; + internal double EffectiveContentHeight => ContentBounds.Height / ContentScale; + /// /// GSUB (glyph substitution) features to request when shaping text for this export, e.g. /// ligatures and contextual alternates. Defaults to | diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index d3f716e1a..4fdfe7154 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -1481,8 +1481,8 @@ private static double RangeRowHeight(PdfRange range, int row) internal static Pages GetNumberOfPages(PdfPageSettings pageSettings, PdfWorksheet pdfSheet, ref PdfRange range) { - var xPages = (int)Math.Max(1, Math.Ceiling(range.TotalWidth / pageSettings.ContentBounds.Width)); - var yPages = (int)Math.Max(1, Math.Ceiling(range.TotalHeight / pageSettings.ContentBounds.Height)); + var xPages = (int)Math.Max(1, Math.Ceiling(range.TotalWidth / pageSettings.EffectiveContentWidth)); + var yPages = (int)Math.Max(1, Math.Ceiling(range.TotalHeight / pageSettings.EffectiveContentHeight)); if (pageSettings.ShowHeadings) { @@ -1491,13 +1491,13 @@ internal static Pages GetNumberOfPages(PdfPageSettings pageSettings, PdfWorkshee { prev = xPages; range.AdditionalWidth = xPages * ((rowHeadingWith1CharWidth - pdfSheet.ZeroCharWidth) + (Math.Abs(pdfSheet.ToRow).ToString().Length * pdfSheet.ZeroCharWidth)); - xPages = (int)Math.Max(1, Math.Ceiling((range.TotalWidth + range.AdditionalWidth) / pageSettings.ContentBounds.Width)); + xPages = (int)Math.Max(1, Math.Ceiling((range.TotalWidth + range.AdditionalWidth) / pageSettings.EffectiveContentWidth)); } while (prev != xPages); do { prev = yPages; range.AdditionalHeight = yPages * pdfSheet.Worksheet.DefaultRowHeight; - yPages = (int)Math.Max(1, Math.Ceiling((range.TotalHeight + range.AdditionalHeight) / pageSettings.ContentBounds.Height)); + yPages = (int)Math.Max(1, Math.Ceiling((range.TotalHeight + range.AdditionalHeight) / pageSettings.EffectiveContentHeight)); } while (prev != yPages); } for (int i = range.Range._fromCol; i <= range.Range._toCol; i++) @@ -1584,7 +1584,7 @@ private static List GetColumnSegments(PdfPageSettings pageSettings, int actualCol = range.Range._fromCol + col; bool reserveTitle = titleWidth > 0d && printTitleColTo >= 0 && (range.Map.FromColumn + segStartIdx) > printTitleColTo; double effectiveAdded = addedWidth + (reserveTitle ? titleWidth : 0d); - if (width + range.ColWidths[col] + effectiveAdded >= pageSettings.ContentBounds.Width) + if (width + range.ColWidths[col] + effectiveAdded >= pageSettings.EffectiveContentWidth) { if (col == segStartIdx) { @@ -1622,7 +1622,7 @@ private static List GetRowSegments(PdfPageSettings pageSettings, Pd int actualRow = range.Range._fromRow + row; bool reserveTitle = titleHeight > 0d && printTitleRowTo >= 0 && (range.Map.FromRow + segStartIdx) > printTitleRowTo; double effectiveAdded = addedHeight + (reserveTitle ? titleHeight : 0d); - if (height + range.RowHeights[row].Height + effectiveAdded >= pageSettings.ContentBounds.Height) + if (height + range.RowHeights[row].Height + effectiveAdded >= pageSettings.EffectiveContentHeight) { segments.Add(new PageSegment(range.Map.FromRow + segStartIdx, range.Map.FromRow + row - 1)); segStartIdx = row; diff --git a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs index 5aff5a026..d64efb455 100644 --- a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs +++ b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs @@ -44,7 +44,7 @@ private static void ApplyPrinterSettings(PdfPageSettings settings, ExcelPrinterS settings.Margins = new PdfMargins(topMargin, bottomMargin, leftMargin, rightMargin, headerMargin, footerMargin); settings.Orientation = (Orientations)eps.Orientation; //Scaling is not yet implemented. - settings.Scaling = new PdfScaling(eps.Scale); + settings.Scaling = new PdfScaling(eps.Scale / 100d); settings.ShowHeadings = eps.ShowHeaders; settings.RowsToRepeatAtTop = eps.RepeatRows != null ? eps.RepeatRows.Address : null; settings.ColumnsToRepeatAtLeft = eps.RepeatColumns != null ? eps.RepeatColumns.Address : null; From 246d6c64394d37ac013d76f8a5bebd5aa692d0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 22 Sep 2026 16:56:58 +0200 Subject: [PATCH 02/17] Progress --- .../DocumentObjects/PdfContentStream.cs | 12 ++++++++++-- src/EPPlus.Export.Pdf/ExcelPdf.cs | 13 +++++++++++++ src/EPPlus.Export.Pdf/Layout/PdfCellLayout.cs | 10 ++++++++-- src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs | 5 ++++- .../TextMapping/PdfHeaderFooterCollection.cs | 4 ++-- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs index d12b983fc..d02acd436 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs @@ -47,6 +47,14 @@ public void AddCommand(string command) commands.Add(command); } + public void AddContentScale(double scale, double anchorX, double anchorY) + { + if (scale == 1d) return; + double e = (1d - scale) * anchorX; + double f = (1d - scale) * anchorY; + commands.Add($"{scale.ToPdfString()} 0 0 {scale.ToPdfString()} {e.ToPdfString()} {f.ToPdfString()} cm"); + } + public void AddCellLayout(PdfCellLayout cell, string label) { if (cell.Size.X <= 0d || cell.Size.Y <= 0d) return; @@ -476,8 +484,8 @@ public void AddMarginClipping(PdfPageLayout pageLayout, PdfPageSettings pageSett left = System.Math.Min(left, System.Math.Min(line.X1, line.X2)); right = System.Math.Max(right, System.Math.Max(line.X1, line.X2)); } - right = System.Math.Min(right, pageSettings.PageSize.WidthPu); - bottom = System.Math.Max(bottom, 0d); + //right = System.Math.Min(right, pageSettings.PageSize.WidthPu); + //bottom = System.Math.Max(bottom, 0d); var pad = GridLine.Width * 4; var x = left + pl.HeadingWidth + pl.PrintTitleWidth - pad; var y = bottom - pad; diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 0db79f7c2..6da361832 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -178,6 +178,9 @@ private PdfCatalog AddCatalog(int pagesObjectNumber) private void AddContent(PdfPageLayout pageLayout, PdfPage page) { var pageSettings = pageLayout.Settings; + double contentScale = pageSettings.ContentScale; + double scaleAnchorX = pageSettings.ContentBounds.Left; + double scaleAnchorY = pageSettings.ContentBounds.Top; var cells = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout || t is PdfCellContentLayout || t is PdfCellBorderLayout) && !(t is PdfCellLayout cc && (cc.IsHeading || cc.IsPrintTitle)) && @@ -190,6 +193,7 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddCommand($"% {pageLayout.Name} start"); //Start page content clipping rectangle. contentStream.AddCommand("q"); + contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); contentStream.AddMarginClipping((PdfPageLayout)pageLayout, pageSettings); if (pageSettings.ShowGridLines) { @@ -221,6 +225,8 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddCommand("Q"); contentStream.AddCommand($"% Margin Clip End"); //Add headings + contentStream.AddCommand("q"); + contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); foreach (var heading in headingLayouts) { contentStream.AddCommand($"% HEADING : {heading.Name}"); @@ -234,11 +240,15 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddBorderLayout(borderLayout); break; } } + contentStream.AddCommand("Q"); //Add outer gridlines if (pageSettings.ShowGridLines || pageSettings.ShowHeadings) { + contentStream.AddCommand("q"); + contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); contentStream.AddOuterGridBorder(pageLayout); contentStream.AddPrintTitleGridLines(pageLayout); + contentStream.AddCommand("Q"); } //Add header and footer. foreach (var hf in headerFooterLayouts) @@ -254,6 +264,8 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) if (PdfImageXObject.ProducesSoftMask(image.ImageBytes)) page.HasTransparency = true; } //Add print titles + contentStream.AddCommand("q"); + contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); foreach (var titleCell in printTitleLayouts) { contentStream.AddCommand($"% PRINT TITLE : {titleCell.Name}"); @@ -267,6 +279,7 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddBorderLayout(borderLayout); break; } } + contentStream.AddCommand("Q"); _document.Add(contentStream); page.contentObjectNumbers.Add(contentStream.objectNumber); contentStream.AddCommand($"% {pageLayout.Name} end"); diff --git a/src/EPPlus.Export.Pdf/Layout/PdfCellLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfCellLayout.cs index 59774822d..c0967c411 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfCellLayout.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfCellLayout.cs @@ -137,7 +137,10 @@ public void UpdateShadingPositionMatrix(PdfPageSettings pageSettings) CellFillData.GradientFillData.coords = [0, 0, 1, 0]; break; } - CellFillData.GradientFillData.matrix = [Size.X, 0, 0, Size.Y, LocalPosition.X, LocalPosition.Y]; + double s = pageSettings.ContentScale; + double ax = pageSettings.ContentBounds.Left; + double ay = pageSettings.ContentBounds.Top; + CellFillData.GradientFillData.matrix = [Size.X * s, 0, 0, Size.Y * s, LocalPosition.X * s + (1d - s) * ax, LocalPosition.Y * s + (1d - s) * ay]; } else if (CellFillData.GradientFillData.GradientType == ExcelFillGradientType.Path) { @@ -170,7 +173,10 @@ public void UpdateShadingPositionMatrix(PdfPageSettings pageSettings) { CellFillData.GradientFillData.coords = [0.5, 0.5, 0, 0.5, 0.5, r]; } - CellFillData.GradientFillData.matrix = [Size.X, 0, 0, Size.Y, LocalPosition.X, LocalPosition.Y]; + double s = pageSettings.ContentScale; + double ax = pageSettings.ContentBounds.Left; + double ay = pageSettings.ContentBounds.Top; + CellFillData.GradientFillData.matrix = [Size.X * s, 0, 0, Size.Y * s, LocalPosition.X * s + (1d - s) * ax, LocalPosition.Y * s + (1d - s) * ay]; } } } diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 4fdfe7154..067a9f9f0 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -2029,7 +2029,10 @@ internal static double GetOriginY(PdfPageSettings pageSettings, Page page) internal static double GetClampedCellWidth(PdfPageSettings pageSettings, double cellX, double cellWidth) { - return System.Math.Min(cellWidth, pageSettings.PageSize.WidthPu - cellX); + double s = pageSettings.ContentScale; + double pageRightNatural = pageSettings.ContentBounds.Left + + (pageSettings.PageSize.WidthPu - pageSettings.ContentBounds.Left) / s; + return System.Math.Min(cellWidth, pageRightNatural - cellX); } } } diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs index 1d345ccd8..c7204d51f 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs @@ -34,8 +34,8 @@ public PdfHeaderFooterCollection(PdfPageSettings pageSettings, PdfDictionaries d bool differentOddEven = pdfSheet.Worksheet.HeaderFooter.differentOddEven; HasFirstPage = differentFirst; HasOddEvenPages = differentOddEven; - bool AlignWithMargins = pdfSheet.Worksheet.HeaderFooter.AlignWithMargins; - bool ScaleWithDocument = pdfSheet.Worksheet.HeaderFooter.ScaleWithDocument; + AlignWithMargins = pdfSheet.Worksheet.HeaderFooter.AlignWithMargins; + ScaleWithDocument = pdfSheet.Worksheet.HeaderFooter.ScaleWithDocument; PdfHeaderFooter entry = null; if (differentFirst) { From b1593ac74ec9126694537ad28cda9cc8ae7cccde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 23 Sep 2026 14:19:55 +0200 Subject: [PATCH 03/17] Scaling header and footer --- src/EPPlus/ExcelHeaderFooter.cs | 2 +- .../TextMapping/PdfHeaderFooterCollection.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/ExcelHeaderFooter.cs b/src/EPPlus/ExcelHeaderFooter.cs index a7ac0f6cd..894ecd352 100644 --- a/src/EPPlus/ExcelHeaderFooter.cs +++ b/src/EPPlus/ExcelHeaderFooter.cs @@ -435,7 +435,7 @@ public bool ScaleWithDocument { get { - return GetXmlNodeBool(scaleWithDocPath); + return GetXmlNodeBool(scaleWithDocPath, true); } set { diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs index c7204d51f..130d29f9d 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs @@ -157,6 +157,21 @@ public PdfHeaderFooterCollection(PdfPageSettings pageSettings, PdfDictionaries d entry.Content.ContentAligmnet = PdfTextMap.GetAlignmentData(entry); PdfHeaderFooterEntries.Add(entry); } + + double hfScale = pageSettings.ContentScale; + if (ScaleWithDocument && hfScale != 1d) + { + foreach (var e in PdfHeaderFooterEntries) + { + if (e.Content?.TextFragments != null) + { + foreach (var tf in e.Content.TextFragments) + tf.Font.Size = (float)(tf.Font.Size * hfScale); + } + e.ImageWidth *= hfScale; + e.ImageHeight *= hfScale; + } + } } public PdfHeaderFooter Get(HeaderFooterType type, HeaderFooterSection section, HeaderFooterAlignment alignment) From 4e1303d100c838b9b3c19d79c078fe89c2009983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Thu, 24 Sep 2026 09:31:36 +0200 Subject: [PATCH 04/17] Added fit to page test. --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index c6e49ad1d..7026ef0ae 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -1137,5 +1137,17 @@ public void ScalingTests() string path = _pdfPath + "ScalingTest1.pdf"; ws.SaveAsPdf(path); } + + [TestMethod] + public void ScalingTestFitToPage() + { + using var p = OpenTemplatePackage("PDFTest.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 1; + ws.PrinterSettings.FitToHeight = 3; + string path = _pdfPath + "ScalingTest1.pdf"; + ws.SaveAsPdf(path); + } } } From b65aa600af8114fe311be31fcb3f4884bf9222ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Thu, 1 Oct 2026 16:51:30 +0200 Subject: [PATCH 05/17] scaling progress --- .../MeasurementRenderWidthParityTests.cs | 4 +- .../MultipleSubstitutionReproTests.cs | 2 +- src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs | 8 +-- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 13 ++-- .../VariationSequenceReproTests.cs | 2 +- .../Settings/PdfPageSettings.cs | 15 +++-- src/EPPlus/ExcelRangeBase.cs | 8 +-- src/EPPlus/ExcelWorkbook.cs | 24 +++---- src/EPPlus/ExcelWorksheet.cs | 8 +-- .../Export/PdfExport/Layout/PdfLayout.cs | 67 ++++++++++++++++--- src/EPPlus/Export/PdfExport/PdfCatalog.cs | 46 ++++++++----- .../PdfExport/Settings/GetPdfSettings.cs | 3 +- .../PdfExport/TextMapping/PdfTextMap.cs | 2 +- 13 files changed, 130 insertions(+), 72 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/MeasurementRenderWidthParityTests.cs b/src/EPPlus.Export.Pdf.Tests/MeasurementRenderWidthParityTests.cs index 9c0f5b494..060b68f27 100644 --- a/src/EPPlus.Export.Pdf.Tests/MeasurementRenderWidthParityTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/MeasurementRenderWidthParityTests.cs @@ -125,7 +125,7 @@ public void MeasuredWidth_MatchesRenderedWidth_ForKernedText() private static double GetMeasuredWidth(PdfPageSettings settings, ExcelRangeBase range) { - var catalog = new PdfCatalog(); + var catalog = new EPPlusToPdfWriter(); var cells = catalog.GetCellCollectionFromRange(settings, range); return cells[range.Start.Row, range.Start.Column].TotalTextLength; } @@ -134,7 +134,7 @@ private static byte[] RenderToBytes(PdfPageSettings settings, ExcelWorksheet she { using (var stream = new MemoryStream()) { - new PdfCatalog(settings, sheet).Save(stream); + new EPPlusToPdfWriter(settings, sheet).Save(stream); return stream.ToArray(); } } diff --git a/src/EPPlus.Export.Pdf.Tests/MultipleSubstitutionReproTests.cs b/src/EPPlus.Export.Pdf.Tests/MultipleSubstitutionReproTests.cs index 1d05cb028..9c3abbd5c 100644 --- a/src/EPPlus.Export.Pdf.Tests/MultipleSubstitutionReproTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/MultipleSubstitutionReproTests.cs @@ -161,7 +161,7 @@ public void MultipleSubstitution_Roboto_ReproSheet() // rather than silently writing an unreadable file. using (var stream = new MemoryStream()) { - new PdfCatalog(settings, sheet).Save(stream); + new EPPlusToPdfWriter(settings, sheet).Save(stream); AssertLooksLikePdf(stream.ToArray()); } diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs b/src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs index db7244176..c069cf883 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs @@ -78,7 +78,7 @@ protected void SaveAsPdf(ExcelWorksheet sheet, string pdfFileName, PdfPageSettin Directory.CreateDirectory(_pdfPath); } var path = Path.Combine(_pdfPath, pdfFileName); - new PdfCatalog(settings, sheet).Save(path); + new EPPlusToPdfWriter(settings, sheet).Save(path); } /// @@ -92,7 +92,7 @@ protected void SaveAsPdf(ExcelWorkbook wb, string pdfFileName, PdfPageSettings s pdfFileName += ".pdf"; } var path = Path.Combine(_pdfPath, pdfFileName); - new PdfCatalog(settings, wb).Save(path); + new EPPlusToPdfWriter(settings, wb).Save(path); } /// @@ -107,9 +107,9 @@ protected void SaveAsPdf(ExcelWorkbook wb, string pdfFileName, PdfPageSettings s } var path = Path.Combine(_pdfPath, pdfFileName); if (ranges.Count() > 1) - new PdfCatalog(settings, ranges).Save(path); + new EPPlusToPdfWriter(settings, ranges).Save(path); else - new PdfCatalog(settings, ranges[0]).Save(path); + new EPPlusToPdfWriter(settings, ranges[0]).Save(path); } } } \ No newline at end of file diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 7026ef0ae..194f904da 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -376,7 +376,7 @@ public void SaveWorksheetToStreamViaCatalogTest() var ws = p.Workbook.Worksheets[0]; var pageSettings = new PdfPageSettings(ws.Workbook.RenderContext.FontEngine); using var ms = new MemoryStream(); - var pdfCatalog = new PdfCatalog(pageSettings, ws); + var pdfCatalog = new EPPlusToPdfWriter(pageSettings, ws); pdfCatalog.Save(ms); AssertLooksLikePdf(ms.ToArray()); } @@ -674,7 +674,7 @@ public void WorkbookTests(string sourceFile, string outputPath, string wsName) pageSettings.ShowGridLines = false; pageSettings.ShowHeadings = false; - var pdfCatalog = new PdfCatalog(pageSettings, ws); + var pdfCatalog = new EPPlusToPdfWriter(pageSettings, ws); pdfCatalog.Save(outputPath); } @@ -813,7 +813,7 @@ public void EachWorksheetUsesItsOwnOrientation() byte[] pdf; using (var ms = new MemoryStream()) { - var pdfCatalog = new PdfCatalog(settings, package.Workbook); + var pdfCatalog = new EPPlusToPdfWriter(settings, package.Workbook); pdfCatalog.Save(ms); pdf = ms.ToArray(); } @@ -880,7 +880,7 @@ public void EachWorksheetUsesItsOwnPaperSize() byte[] pdf; using (var ms = new MemoryStream()) { - var pdfCatalog = new PdfCatalog(settings, package.Workbook); + var pdfCatalog = new EPPlusToPdfWriter(settings, package.Workbook); pdfCatalog.Save(ms); pdf = ms.ToArray(); } @@ -1143,10 +1143,11 @@ public void ScalingTestFitToPage() { using var p = OpenTemplatePackage("PDFTest.xlsx"); var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.RepeatRows = new ExcelAddress("1:2"); ws.PrinterSettings.FitToPage = true; - ws.PrinterSettings.FitToWidth = 1; + ws.PrinterSettings.FitToWidth = 2; ws.PrinterSettings.FitToHeight = 3; - string path = _pdfPath + "ScalingTest1.pdf"; + string path = _pdfPath + "ScalingTest2.pdf"; ws.SaveAsPdf(path); } } diff --git a/src/EPPlus.Export.Pdf.Tests/VariationSequenceReproTests.cs b/src/EPPlus.Export.Pdf.Tests/VariationSequenceReproTests.cs index 1f24b5e6a..188cf9835 100644 --- a/src/EPPlus.Export.Pdf.Tests/VariationSequenceReproTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/VariationSequenceReproTests.cs @@ -88,7 +88,7 @@ public void VariationSequence_BizUdGothic_ReproSheet() // rather than silently writing an unreadable file. using (var stream = new MemoryStream()) { - new PdfCatalog(settings, sheet).Save(stream); + new EPPlusToPdfWriter(settings, sheet).Save(stream); AssertLooksLikePdf(stream.ToArray()); } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index 5a747d226..24e1bad2d 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -196,17 +196,18 @@ public PdfScaling Scaling } } + private double _resolvedFitScale = 1d; + internal void SetResolvedFitScale(double s) { _resolvedFitScale = s; } + internal double ContentScale { get { - if (_scaling != null && - _scaling.ScalingMode == ScalingMode.AdjustToNormalSize && - _scaling.Scale > 0d) - { - return _scaling.Scale; - } - return 1d; + if (_scaling == null) + return 1d; + if (_scaling.ScalingMode == ScalingMode.AdjustToNormalSize) + return _scaling.Scale > 0d ? _scaling.Scale : 1d; + return _resolvedFitScale; } } internal double EffectiveContentWidth => ContentBounds.Width / ContentScale; diff --git a/src/EPPlus/ExcelRangeBase.cs b/src/EPPlus/ExcelRangeBase.cs index 652054336..4c44a0cdf 100644 --- a/src/EPPlus/ExcelRangeBase.cs +++ b/src/EPPlus/ExcelRangeBase.cs @@ -1146,7 +1146,7 @@ public IExcelHtmlRangeExporter CreateHtmlExporter() public void SaveAsPdf(string fileName) { var setttings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this.Worksheet.Workbook, this.Worksheet.PrinterSettings); - var pdfCatalog = new PdfCatalog(setttings, this); + var pdfCatalog = new EPPlusToPdfWriter(setttings, this); pdfCatalog.Save(fileName); } @@ -1162,7 +1162,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(fileName); }, cancellationToken); } @@ -1174,7 +1174,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken public void SaveAsPdf(Stream stream) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this.Worksheet.Workbook, this.Worksheet.PrinterSettings); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(stream); } @@ -1190,7 +1190,7 @@ public Task SaveAsPdfAsync(Stream stream, CancellationToken cancellationToken = return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(stream); }, cancellationToken); } diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 16b5b2cef..70608281c 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -750,7 +750,7 @@ public IExcelHtmlRangeExporter CreateHtmlExporter(params ExcelRangeBase[] ranges public void SaveAsPdf(string fileName) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, Worksheets[View.ActiveTab].PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, this); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, this); catalog.Save(fileName); } @@ -762,7 +762,7 @@ public void SaveAsPdf(string fileName) public void SaveAsPdf(string fileName, params ExcelWorksheet[] worksheets) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, worksheets[0].PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, worksheets); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, worksheets); catalog.Save(fileName); } @@ -774,7 +774,7 @@ public void SaveAsPdf(string fileName, params ExcelWorksheet[] worksheets) public void SaveAsPdf(string fileName, params ExcelRangeBase[] ranges) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, ranges[0].Worksheet.PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, ranges); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, ranges); catalog.Save(fileName); } @@ -790,7 +790,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(fileName); }, cancellationToken); } @@ -817,7 +817,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken, return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, worksheets); + var pdfCatalog = new EPPlusToPdfWriter(settings, worksheets); pdfCatalog.Save(fileName); }, cancellationToken); } @@ -844,7 +844,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken, return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, ranges); + var pdfCatalog = new EPPlusToPdfWriter(settings, ranges); pdfCatalog.Save(fileName); }, cancellationToken); } @@ -856,7 +856,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken, public void SaveAsPdf(Stream stream) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, Worksheets[View.ActiveTab].PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, this); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, this); catalog.Save(stream); } @@ -868,7 +868,7 @@ public void SaveAsPdf(Stream stream) public void SaveAsPdf(Stream stream, params ExcelWorksheet[] worksheets) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, worksheets[0].PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, worksheets); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, worksheets); catalog.Save(stream); } @@ -880,7 +880,7 @@ public void SaveAsPdf(Stream stream, params ExcelWorksheet[] worksheets) public void SaveAsPdf(Stream stream, params ExcelRangeBase[] ranges) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this, ranges[0].Worksheet.PrinterSettings); - PdfCatalog catalog = new PdfCatalog(settings, ranges); + EPPlusToPdfWriter catalog = new EPPlusToPdfWriter(settings, ranges); catalog.Save(stream); } @@ -896,7 +896,7 @@ public Task SaveAsPdfAsync(Stream stream, CancellationToken cancellationToken = return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(stream); }, cancellationToken); } @@ -923,7 +923,7 @@ public Task SaveAsPdfAsync(Stream stream, CancellationToken cancellationToken, p return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, worksheets); + var pdfCatalog = new EPPlusToPdfWriter(settings, worksheets); pdfCatalog.Save(stream); }, cancellationToken); } @@ -950,7 +950,7 @@ public Task SaveAsPdfAsync(Stream stream, CancellationToken cancellationToken, p return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, ranges); + var pdfCatalog = new EPPlusToPdfWriter(settings, ranges); pdfCatalog.Save(stream); }, cancellationToken); } diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index cc060ec75..997917740 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -3745,7 +3745,7 @@ internal bool IsDisposed public void SaveAsPdf(string fileName) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this.Workbook, PrinterSettings); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(fileName); } @@ -3761,7 +3761,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(fileName); }, cancellationToken); } @@ -3773,7 +3773,7 @@ public Task SaveAsPdfAsync(string fileName, CancellationToken cancellationToken public void SaveAsPdf(Stream stream) { var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(this.Workbook, PrinterSettings); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(stream); } @@ -3789,7 +3789,7 @@ public Task SaveAsPdfAsync(Stream stream, CancellationToken cancellationToken = return Task.Run(() => { cancellationToken.ThrowIfCancellationRequested(); - var pdfCatalog = new PdfCatalog(settings, this); + var pdfCatalog = new EPPlusToPdfWriter(settings, this); pdfCatalog.Save(stream); }, cancellationToken); } diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 067a9f9f0..9d2d9d3c5 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -15,6 +15,7 @@ Date Author Change using EPPlus.Export.Pdf.Layout; using EPPlus.Export.Pdf.Resources; using EPPlus.Export.Pdf.Settings; +using EPPlus.Export.Pdf.Settings.PdfPageSizes; using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; @@ -926,6 +927,10 @@ internal static List GetPages(PdfPageSettings[] sheetSettings, PdfWorkshe var pdfSheet = pdfSheets[si]; var pageSettings = sheetSettings[si]; + if (pdfSheet.Ranges.Count > 0) + { + ResolveFitScale(pageSettings, pdfSheet, pdfSheet.Ranges[0]); + } for (int ri = 0; ri < pdfSheet.Ranges.Count; ri++) { var range = pdfSheet.Ranges[ri]; @@ -1500,15 +1505,18 @@ internal static Pages GetNumberOfPages(PdfPageSettings pageSettings, PdfWorkshee yPages = (int)Math.Max(1, Math.Ceiling((range.TotalHeight + range.AdditionalHeight) / pageSettings.EffectiveContentHeight)); } while (prev != yPages); } - for (int i = range.Range._fromCol; i <= range.Range._toCol; i++) - { - if (pdfSheet.Worksheet.Column(i).PageBreak) - xPages++; - } - for (int i = range.Range._fromRow; i <= range.Range._toRow; i++) + if (pageSettings.Scaling?.ScalingMode != ScalingMode.FitToPages) { - if (pdfSheet.Worksheet.Row(i).PageBreak) - yPages++; + for (int i = range.Range._fromCol; i <= range.Range._toCol; i++) + { + if (pdfSheet.Worksheet.Column(i).PageBreak) + xPages++; + } + for (int i = range.Range._fromRow; i <= range.Range._toRow; i++) + { + if (pdfSheet.Worksheet.Row(i).PageBreak) + yPages++; + } } ComputePrintTitleDimensions(pdfSheet, range, out range.PrintTitleHeight, out range.PrintTitleWidth); range.PrintTitleRowTo = pdfSheet.PrintTitleRowFrom >= 0 ? pdfSheet.PrintTitleRowTo : -1; @@ -1600,7 +1608,7 @@ private static List GetColumnSegments(PdfPageSettings pageSettings, continue; } width += range.ColWidths[col]; - if (worksheet.Column(actualCol).PageBreak) + if (pageSettings.Scaling?.ScalingMode != ScalingMode.FitToPages && worksheet.Column(actualCol).PageBreak) { segments.Add(new PageSegment(range.Map.FromColumn + segStartIdx, range.Map.FromColumn + col)); segStartIdx = col + 1; @@ -1631,7 +1639,7 @@ private static List GetRowSegments(PdfPageSettings pageSettings, Pd continue; } height += range.RowHeights[row].Height; - if (worksheet.Row(actualRow).PageBreak) + if (pageSettings.Scaling?.ScalingMode != ScalingMode.FitToPages && worksheet.Row(actualRow).PageBreak) { segments.Add(new PageSegment(range.Map.FromRow + segStartIdx, range.Map.FromRow + row)); segStartIdx = row + 1; @@ -2034,5 +2042,44 @@ internal static double GetClampedCellWidth(PdfPageSettings pageSettings, double + (pageSettings.PageSize.WidthPu - pageSettings.ContentBounds.Left) / s; return System.Math.Min(cellWidth, pageRightNatural - cellX); } + + private static void ResolveFitScale(PdfPageSettings pageSettings, PdfWorksheet pdfSheet, PdfRange range) + { + var scaling = pageSettings.Scaling; + if (scaling == null || scaling.ScalingMode != ScalingMode.FitToPages) return; + + int N = scaling.PagesWide; // 0 = automatic + int M = scaling.PagesTall; + var worksheet = range.Range.Worksheet; + + // Same per-page reservations the real pagination uses (both are per-page constants — no circularity). + double addedWidth = pageSettings.ShowHeadings + ? (rowHeadingWith1CharWidth - pdfSheet.ZeroCharWidth) + (System.Math.Abs(pdfSheet.ToRow).ToString().Length * pdfSheet.ZeroCharWidth) + : 0d; + double addedHeight = pageSettings.ShowHeadings ? pdfSheet.Worksheet.DefaultRowHeight : 0d; + + ComputePrintTitleDimensions(pdfSheet, range, out double titleHeight, out double titleWidth); + int titleColTo = pdfSheet.PrintTitleColFrom >= 0 ? pdfSheet.PrintTitleColTo : -1; + int titleRowTo = pdfSheet.PrintTitleRowFrom >= 0 ? pdfSheet.PrintTitleRowTo : -1; + + bool Fits(double s) + { + pageSettings.SetResolvedFitScale(s); + bool okW = N <= 0 || GetColumnSegments(pageSettings, range, worksheet, addedWidth, titleWidth, titleColTo).Count <= N; + bool okH = M <= 0 || GetRowSegments(pageSettings, range, worksheet, addedHeight, titleHeight, titleRowTo).Count <= M; + return okW && okH; + } + + if (Fits(1.0d)) { pageSettings.SetResolvedFitScale(1.0d); return; } // fits at 100% → don't shrink + + // Largest scale in [0.1, 1.0] whose real pagination fits N×M (monotonic: fewer pages as scale shrinks). + double lo = 0.1d, hi = 1.0d; + for (int i = 0; i < 50; i++) + { + double mid = (lo + hi) * 0.5d; + if (Fits(mid)) lo = mid; else hi = mid; + } + pageSettings.SetResolvedFitScale(lo); + } } } diff --git a/src/EPPlus/Export/PdfExport/PdfCatalog.cs b/src/EPPlus/Export/PdfExport/PdfCatalog.cs index 0e0d05434..a91518537 100644 --- a/src/EPPlus/Export/PdfExport/PdfCatalog.cs +++ b/src/EPPlus/Export/PdfExport/PdfCatalog.cs @@ -36,7 +36,7 @@ namespace OfficeOpenXml.Export.PdfExport /// The constructors only store the input - they do not perform any work. Call /// or to actually build the PDF and write it to disk/stream. /// - internal class PdfCatalog + internal class EPPlusToPdfWriter { internal PdfDictionaries _dictionaries = new PdfDictionaries(); private bool _addTextForHeadings = true; @@ -47,26 +47,26 @@ internal class PdfCatalog private readonly PdfPageSettings _pageSettings; private readonly Action> _build; - public PdfCatalog() { } + public EPPlusToPdfWriter() { } // // CONSTRUCTORS FOR MULTIPLE WORKSHEETS AS INPUT // - public PdfCatalog(PdfPageSettings pageSettings, ExcelWorkbook workbook) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelWorkbook workbook) { _pageSettings = pageSettings; var worksheets = workbook.Worksheets.ToArray(); _build = writePdf => HandleWorksheetCollection(pageSettings, worksheets, writePdf); } - public PdfCatalog(PdfPageSettings pageSettings, ExcelWorksheet[] worksheets) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelWorksheet[] worksheets) { _pageSettings = pageSettings; _build = writePdf => HandleWorksheetCollection(pageSettings, worksheets, writePdf); } - public PdfCatalog(PdfPageSettings pageSettings, List worksheets) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, List worksheets) : this(pageSettings, worksheets.ToArray()) { } @@ -116,7 +116,7 @@ private void HandleWorksheetCollection(PdfPageSettings pageSettings, ExcelWorksh // CONSTRUCTORS FOR SINGLE WORKSHEET AS INPUT // - public PdfCatalog(PdfPageSettings pageSettings, ExcelWorksheet worksheet) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelWorksheet worksheet) { _pageSettings = pageSettings; _build = writePdf => BuildPdf(pageSettings, worksheet, writePdf); @@ -160,7 +160,7 @@ private void BuildPdf(PdfPageSettings pageSettings, ExcelWorksheet worksheet, Ac // CONSTRUCTORS FOR RANGE AS INPUT // - public PdfCatalog(PdfPageSettings pageSettings, ExcelRangeBase range) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelRangeBase range) { _pageSettings = pageSettings; _build = writePdf => BuildPdfFromRange(pageSettings, range, writePdf); @@ -190,13 +190,13 @@ private void BuildPdfFromRange(PdfPageSettings pageSettings, ExcelRangeBase rang } } - public PdfCatalog(PdfPageSettings pageSettings, ExcelRangeBase[] ranges) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelRangeBase[] ranges) { _pageSettings = pageSettings; _build = writePdf => HandleRangeCollection(pageSettings, ranges, writePdf); } - public PdfCatalog(PdfPageSettings pageSettings, List ranges) + public EPPlusToPdfWriter(PdfPageSettings pageSettings, List ranges) : this(pageSettings, ranges.ToArray()) { } @@ -486,17 +486,27 @@ private void GetPrintTitles(PdfPageSettings pageSettings, PdfWorksheet pdfSheet) if (worksheet.Names.ContainsKey("_xlnm.Print_Titles")) { var printTitlesName = worksheet.Names["_xlnm.Print_Titles"]; - foreach (var address in printTitlesName.Addresses) + if (printTitlesName.Addresses == null) { - if (address._toCol >= ExcelPackage.MaxColumns) - { - pdfSheet.PrintTitleRowFrom = address._fromRow; - pdfSheet.PrintTitleRowTo = address._toRow; - } - else if (address._toRow >= ExcelPackage.MaxRows) + pdfSheet.PrintTitleRowFrom = printTitlesName._fromRow; + pdfSheet.PrintTitleRowTo = printTitlesName._toRow; + pdfSheet.PrintTitleColFrom = printTitlesName._fromCol; + pdfSheet.PrintTitleColTo = printTitlesName._toCol; + } + else + { + foreach (var address in printTitlesName.Addresses) { - pdfSheet.PrintTitleColFrom = address._fromCol; - pdfSheet.PrintTitleColTo = address._toCol; + if (address._toCol >= ExcelPackage.MaxColumns) + { + pdfSheet.PrintTitleRowFrom = address._fromRow; + pdfSheet.PrintTitleRowTo = address._toRow; + } + else if (address._toRow >= ExcelPackage.MaxRows) + { + pdfSheet.PrintTitleColFrom = address._fromCol; + pdfSheet.PrintTitleColTo = address._toCol; + } } } } diff --git a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs index d64efb455..1e38fa97b 100644 --- a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs +++ b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs @@ -44,14 +44,13 @@ private static void ApplyPrinterSettings(PdfPageSettings settings, ExcelPrinterS settings.Margins = new PdfMargins(topMargin, bottomMargin, leftMargin, rightMargin, headerMargin, footerMargin); settings.Orientation = (Orientations)eps.Orientation; //Scaling is not yet implemented. - settings.Scaling = new PdfScaling(eps.Scale / 100d); + settings.Scaling = eps.FitToPage ? new PdfScaling(eps.FitToWidth, eps.FitToHeight) : new PdfScaling(eps.Scale / 100d); settings.ShowHeadings = eps.ShowHeaders; settings.RowsToRepeatAtTop = eps.RepeatRows != null ? eps.RepeatRows.Address : null; settings.ColumnsToRepeatAtLeft = eps.RepeatColumns != null ? eps.RepeatColumns.Address : null; //Print area is implemented and uses the defined name instead of this setting. this setting should override the print area defined name. settings.PrintArea = eps.PrintArea != null ? eps.PrintArea.Address : null; settings.ShowGridLines = eps.ShowGridLines; - //Centering is not implemented. settings.CenterOnPageHorizontally = eps.HorizontalCentered; settings.CenterOnPageVertically = eps.VerticalCentered; settings.PageOrders = (PageOrders)eps.PageOrder; diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs index afa4bc620..e6e3c94fe 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs @@ -765,7 +765,7 @@ internal static int AddColumnsForNonWrappedText(PdfPageSettings pageSettings, Ex { if (ws.Dimension == null) return 0; //If dimension is null, there are no cells to check, so return 0. int columnsToAdd = 0; - var catalog = new PdfCatalog(); + var catalog = new EPPlusToPdfWriter(); var lastColumn = ws.Dimension.End.Column; ExcelRangeBase lastColumnRange = ws.Cells[1, lastColumn, ws.Dimension.End.Row, lastColumn]; var cc = catalog.GetCellCollectionFromRange(pageSettings, lastColumnRange); From 40117fe3eb1786e2f3537c46405ea27b8d732416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Mon, 5 Oct 2026 14:23:45 +0200 Subject: [PATCH 06/17] Added more tests for scaling --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 161 +++++++++++++++++++++++- 1 file changed, 159 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 194f904da..6a85f1ac5 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -978,7 +978,7 @@ public void headerFooterImage() { using var p = OpenTemplatePackage("EPPlus Sample 3.xlsx"); var ws = p.Workbook.Worksheets[0]; - string path = _pdfPath + "EPPlus Sample 3.pdf"; + string path = _pdfPath + "EPPlus Sample 3 hf1.pdf"; ws.SaveAsPdf(path); } @@ -987,7 +987,7 @@ public void headerFooterImage2() { using var p = CreateWorkbook(); var ws = p.Workbook.Worksheets[0]; - string path = _pdfPath + "EPPlus Sample 3.pdf"; + string path = _pdfPath + "EPPlus Sample 3 hf2.pdf"; ws.SaveAsPdf(path); } public ExcelPackage CreateWorkbook() @@ -1150,5 +1150,162 @@ public void ScalingTestFitToPage() string path = _pdfPath + "ScalingTest2.pdf"; ws.SaveAsPdf(path); } + + + private static int CountPdfPages(string path) + { + var text = Encoding.ASCII.GetString(File.ReadAllBytes(path)); + return Regex.Matches(text, @"/Type\s*/Page\b").Count; + } + + private static ExcelWorksheet AddGrid(ExcelPackage p, int rows, int cols, string name = "Sheet1") + { + var ws = p.Workbook.Worksheets.Add(name); + for (int r = 1; r <= rows; r++) + for (int c = 1; c <= cols; c++) + ws.Cells[r, c].Value = $"R{r}C{c}"; + ws.PrinterSettings.PaperSize = ePaperSize.A4; + ws.PrinterSettings.Orientation = eOrientation.Portrait; + return ws; + } + + private static int SaveAndCount(ExcelWorksheet ws, string name) + { + string path = _pdfPath + name + ".pdf"; + ws.SaveAsPdf(path); + return CountPdfPages(path); + } + + [TestMethod] + public void Percentage_ChangesPageCount() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 120, cols: 20); // multi-page both directions + + ws.PrinterSettings.Scale = 100; int at100 = SaveAndCount(ws, "pct_100"); + ws.PrinterSettings.Scale = 50; int at50 = SaveAndCount(ws, "pct_50"); + ws.PrinterSettings.Scale = 200; int at200 = SaveAndCount(ws, "pct_200"); + + Assert.IsTrue(at100 > 1, "sheet should be multi-page at 100%"); + Assert.IsTrue(at50 < at100, $"50% ({at50}) should use fewer pages than 100% ({at100})"); + Assert.IsTrue(at200 > at100, $"200% ({at200}) should use more pages than 100% ({at100})"); + } + + [TestMethod] + public void Fit_WidthBudget_Exact() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 3, cols: 60); + Assert.IsTrue(SaveAndCount(ws, "fitw_base") > 2, "sheet should span >2 page-columns at 100%"); + + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToHeight = 0; + ws.PrinterSettings.FitToWidth = 1; + Assert.AreEqual(1, SaveAndCount(ws, "fitw_1")); + + ws.PrinterSettings.FitToWidth = 2; + Assert.AreEqual(2, SaveAndCount(ws, "fitw_2")); + } + + [TestMethod] + public void Fit_HeightBudget_Exact() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 300, cols: 3); + Assert.IsTrue(SaveAndCount(ws, "fith_base") > 3); + + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 0; + ws.PrinterSettings.FitToHeight = 3; + Assert.AreEqual(3, SaveAndCount(ws, "fith_3")); + + ws.PrinterSettings.FitToHeight = 1; + Assert.AreEqual(1, SaveAndCount(ws, "fith_1")); + } + + [TestMethod] + public void Fit_OneByOne_SinglePage() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 200, cols: 30); + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 1; + ws.PrinterSettings.FitToHeight = 1; + Assert.AreEqual(1, SaveAndCount(ws, "fit_1x1")); + } + + [TestMethod] + public void Fit_DoesNotEnlargeSmallSheet() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 3, cols: 3); + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 2; + ws.PrinterSettings.FitToHeight = 2; + Assert.AreEqual(1, SaveAndCount(ws, "fit_small")); + } + + [TestMethod] + public void Fit_IgnoresManualPageBreaks() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 300, cols: 3); + ws.Row(150).PageBreak = true; + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 0; + ws.PrinterSettings.FitToHeight = 2; + Assert.AreEqual(2, SaveAndCount(ws, "fit_break")); + } + + [TestMethod] + public void Fit_WithHeadings_HonorsBudget() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 300, cols: 3); + ws.PrinterSettings.ShowHeaders = true; + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 0; + ws.PrinterSettings.FitToHeight = 3; + Assert.AreEqual(3, SaveAndCount(ws, "fit_headings")); + } + + [TestMethod] + public void Fit_WithPrintTitles_HonorsBudget() + { + using var p = new ExcelPackage(); + var ws = AddGrid(p, rows: 300, cols: 3); + ws.PrinterSettings.RepeatRows = new ExcelAddress("1:2"); + ws.PrinterSettings.FitToPage = true; + ws.PrinterSettings.FitToWidth = 0; + ws.PrinterSettings.FitToHeight = 3; + Assert.AreEqual(3, SaveAndCount(ws, "fit_titles")); + } + + [TestMethod] + public void MultiSheet_PerSheetScaling_IsIsolated() + { + using var p = new ExcelPackage(); + var plain = AddGrid(p, rows: 300, cols: 3, name: "Plain"); + var scaled = AddGrid(p, rows: 300, cols: 3, name: "Scaled"); + + int plainAlone = SaveAndCount(plain, "multi_plain"); + int scaledUnscaled = SaveAndCount(scaled, "multi_scaled_off"); + + scaled.PrinterSettings.FitToPage = true; + scaled.PrinterSettings.FitToWidth = 1; + scaled.PrinterSettings.FitToHeight = 1; + int scaledAlone = SaveAndCount(scaled, "multi_scaled_on"); + + Assert.AreEqual(1, scaledAlone, "fit 1x1 should put the scaled sheet on one page"); + Assert.IsTrue(scaledUnscaled > scaledAlone, "scaling should actually reduce the scaled sheet"); + + string combinedPath = _pdfPath + "multi_combined.pdf"; + p.Workbook.SaveAsPdf(combinedPath); + int combined = CountPdfPages(combinedPath); + + Assert.AreEqual(plainAlone + scaledAlone, combined, + $"combined ({combined}) should equal plain ({plainAlone}) + scaled ({scaledAlone}) - " + + "per-sheet scaling must stay isolated"); + } } } From 9fdbd4da681024430a5c6ec703362e7e9f93be8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 7 Oct 2026 15:04:04 +0200 Subject: [PATCH 07/17] Fixed layout order and renamed PdfCatalog to EPPlusToPdfWriter --- src/EPPlus.Export.Pdf/ExcelPdf.cs | 7 +- .../Layout/PdfCellContentLayout.cs | 28 +++--- .../Layout/PdfHeaderFooter.cs | 2 +- .../Settings/PdfPageSettings.cs | 4 +- src/EPPlus/Export/PdfExport/Data/PageData.cs | 1 + .../Export/PdfExport/Data/PdfWorksheet.cs | 1 + .../{PdfCatalog.cs => EPPlusToPdfWriter.cs} | 32 ++++++- .../Export/PdfExport/Layout/PdfLayout.cs | 42 ++++++++- .../PdfExport/Settings/GetPdfSettings.cs | 1 + .../TextMapping/PdfAdditionalContent.cs | 93 +++++++++++++++++++ 10 files changed, 187 insertions(+), 24 deletions(-) rename src/EPPlus/Export/PdfExport/{PdfCatalog.cs => EPPlusToPdfWriter.cs} (95%) create mode 100644 src/EPPlus/Export/PdfExport/TextMapping/PdfAdditionalContent.cs diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 6da361832..83eef4522 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -184,9 +184,10 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) var cells = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout || t is PdfCellContentLayout || t is PdfCellBorderLayout) && !(t is PdfCellLayout cc && (cc.IsHeading || cc.IsPrintTitle)) && - !(t is PdfCellContentLayout ccl && (ccl.IsHeaderFooter || ccl.IsHeading || ccl.IsPrintTitle)) && + !(t is PdfCellContentLayout ccl && (ccl.IsHeaderFooter || ccl.IsHeading || ccl.IsPrintTitle || ccl.IsAddedContent)) && !(t is PdfCellBorderLayout cbl && cbl.IsPrintTitle)).ToList(); var headerFooterLayouts = pageLayout.ChildObjects.OfType().Where(t => t.IsHeaderFooter); + var AdditionalContent = pageLayout.ChildObjects.OfType().Where(t => t.IsAddedContent && !(pageSettings.AdditionalContent != "null")); var headingLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout cl && cl.IsHeading) || (t is PdfCellContentLayout ccl && ccl.IsHeading)); var printTitleLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout pl && pl.IsPrintTitle) || (t is PdfCellContentLayout pcl && pcl.IsPrintTitle) || (t is PdfCellBorderLayout pbl && pbl.IsPrintTitle)); var contentStream = new PdfContentStream(_document.Count + 1); @@ -279,6 +280,10 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddBorderLayout(borderLayout); break; } } + foreach (var content in AdditionalContent) + { + contentStream.AddCellContentLayout(content, _dictionaries, pageSettings); + } contentStream.AddCommand("Q"); _document.Add(contentStream); page.contentObjectNumbers.Add(contentStream.objectNumber); diff --git a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs index 3dc9eeae4..1005c3b5d 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs @@ -29,6 +29,7 @@ internal class PdfCellContentLayout : Transform public bool Clip; public Rect Clipping; public bool IsHeaderFooter; + public bool IsAddedContent; public bool IsHeading; public bool IsPrintTitle; public TextLayoutEngine textLayoutEngine; @@ -40,7 +41,7 @@ internal class PdfCellContentLayout : Transform public List ShapedTexts { get; set; } - public PdfCellContentLayout(PdfPageSettings pageSettings, PdfDictionaries dictionaries, PdfCellBase cell, MergedCellDrawInfo mergedCellInfo, double x, double y, double width, double height, double scaleX = 1, double scaleY = 1, double rotation = 0, Transform parent = null) + public PdfCellContentLayout(PdfPageSettings pageSettings, PdfDictionaries dictionaries, PdfCellBase cell, double x, double y, double width, double height, double scaleX = 1, double scaleY = 1, double rotation = 0, Transform parent = null) : base(x, y-height, width, height, scaleX, scaleY, rotation, parent) { Z = 2; @@ -100,8 +101,21 @@ private double CalculateHorizontalAlignment(string text, double textLength, doub double newX = x; switch (CellAlignmentData.HorizontalAlignment) { + case ExcelHorizontalAlignment.Left: + newX = x + padding; + break; + case ExcelHorizontalAlignment.Center: + case ExcelHorizontalAlignment.CenterContinuous: + newX = x + (width - textLength) / 2d; + break; + case ExcelHorizontalAlignment.Right: + newX = x + (width - textLength) - padding; + break; + case ExcelHorizontalAlignment.Distributed: + case ExcelHorizontalAlignment.Justify: case ExcelHorizontalAlignment.Fill: case ExcelHorizontalAlignment.General: + default: if (double.TryParse(text, out double value)) { newX = x + (width - textLength) - padding; @@ -111,18 +125,6 @@ private double CalculateHorizontalAlignment(string text, double textLength, doub newX = x + padding; } break; - case ExcelHorizontalAlignment.Left: - case ExcelHorizontalAlignment.Justify: - case ExcelHorizontalAlignment.Distributed: - newX = x + padding; - break; - case ExcelHorizontalAlignment.Center: - case ExcelHorizontalAlignment.CenterContinuous: - newX = x + (width - textLength) / 2d; - break; - case ExcelHorizontalAlignment.Right: - newX = x + (width - textLength) - padding; - break; } return newX; } diff --git a/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs index 6831d092b..74923b715 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs @@ -32,7 +32,7 @@ internal enum HeaderFooterAlignment internal enum HeaderFooterSection { Header = 0, - Footer = 1 + Footer = 1, } internal class PdfHeaderFooter diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index 24e1bad2d..fe96ac403 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -12,7 +12,6 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.Pdf.Settings.PdfPageSizes; using EPPlus.Fonts.OpenType; -using OfficeOpenXml; using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; @@ -241,6 +240,8 @@ internal double ContentScale internal PdfContentBounds ContentBounds = new PdfContentBounds(PdfMargins.Normal, PdfPageSize.A4); internal string defaultFontName = ""; + internal string AdditionalContent; + //DEBUG internal bool Debug = false; internal bool PrintAsText = false; @@ -270,6 +271,7 @@ internal PdfPageSettings CloneForSheet() c.GposFeatures = GposFeatures; c.Debug = Debug; c.PrintAsText = PrintAsText; + c.AdditionalContent = AdditionalContent; return c; } } diff --git a/src/EPPlus/Export/PdfExport/Data/PageData.cs b/src/EPPlus/Export/PdfExport/Data/PageData.cs index 444d21966..c8c48b1bd 100644 --- a/src/EPPlus/Export/PdfExport/Data/PageData.cs +++ b/src/EPPlus/Export/PdfExport/Data/PageData.cs @@ -31,6 +31,7 @@ internal struct Page public double PrintTitleHeight; public PdfCellCollection Map; public PdfHeaderFooterCollection HeaderFooters; + public PdfAdditionalContent AddedContent; public Dictionary MergedCells; public List Images; public List PrintTitleCells; diff --git a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs index 9a7c00919..7b3e74167 100644 --- a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs +++ b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs @@ -25,6 +25,7 @@ internal class PdfWorksheet public List Ranges = null; public PdfRange CommentsAndNotes; public PdfHeaderFooterCollection HeaderFooters = null; + public PdfAdditionalContent AdditionalContent = null; public double ZeroCharWidth; public int ToRow; public int PrintTitleRowFrom = -1; diff --git a/src/EPPlus/Export/PdfExport/PdfCatalog.cs b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs similarity index 95% rename from src/EPPlus/Export/PdfExport/PdfCatalog.cs rename to src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs index 3056b22c2..c54d316b7 100644 --- a/src/EPPlus/Export/PdfExport/PdfCatalog.cs +++ b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs @@ -40,6 +40,13 @@ internal class EPPlusToPdfWriter { internal PdfDictionaries _dictionaries = new PdfDictionaries(); private bool _addTextForHeadings = true; + private static bool IsFullEdition + { + get + { + return (int) ExcelPackage.License.LicenseType == (int) EPPlusLicenseType.Commercial; + } + } // Captures which "build" method to run (worksheet collection, single worksheet or range // collection) together with the data it needs, but NOT the destination (file/stream). @@ -75,7 +82,7 @@ private void HandleWorksheetCollection(PdfPageSettings pageSettings, ExcelWorksh { // Match the single-worksheet path: resolve the default font before building. pageSettings.defaultFontName = worksheets[0].Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; - + pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet[] pdfSheets = null; try { @@ -122,9 +129,17 @@ public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelWorksheet worksheet) _build = writePdf => BuildPdf(pageSettings, worksheet, writePdf); } + private static string AddAdditionalContent() + { + if (IsFullEdition) + return "FullEdition"; + return "null"; + } + private void BuildPdf(PdfPageSettings pageSettings, ExcelWorksheet worksheet, Action writePdf) { pageSettings.defaultFontName = worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; + pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet pdfSheet = null; try { @@ -169,7 +184,7 @@ public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelRangeBase range) private void BuildPdfFromRange(PdfPageSettings pageSettings, ExcelRangeBase range, Action writePdf) { pageSettings.defaultFontName = range.Worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; - + pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet pdfSheet = null; try { @@ -204,7 +219,7 @@ public EPPlusToPdfWriter(PdfPageSettings pageSettings, List rang private void HandleRangeCollection(PdfPageSettings pageSettings, ExcelRangeBase[] ranges, Action writePdf) { pageSettings.defaultFontName = ranges[0].Worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; - + pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet[] pdfSheets = null; try { @@ -314,6 +329,9 @@ internal void BuildSubsets(PdfPageSettings pageSettings) internal void ShapeTextInPdfWorksheet(PdfPageSettings pageSettings, PdfWorksheet pdfSheet) { IterateCells(pdfSheet, cell => PdfTextShaper.ShapeText(pageSettings, _dictionaries, cell)); + + if (pdfSheet.AdditionalContent != null) + PdfTextShaper.ShapeText(pageSettings, _dictionaries, pdfSheet.AdditionalContent.content); } private void IterateCells(PdfWorksheet pdfSheet, System.Action action) @@ -363,6 +381,7 @@ internal PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelWorkshe GetMaps(pageSettings, pdfSheet, pdfSheet.Ranges); GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); + GetAdditionalContent(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); ReadDrawings(pdfSheet); return pdfSheet; @@ -379,6 +398,7 @@ private PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelRangeBas pdfSheet.Ranges[0] = GetMaps(pageSettings, pdfSheet, pdfSheet.Ranges[0]); GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); + GetAdditionalContent(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); ReadDrawings(pdfSheet); return pdfSheet; @@ -427,6 +447,7 @@ private PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelWorkshee GetMaps(pageSettings, pdfSheet, pdfSheet.Ranges); GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); + GetAdditionalContent(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); ReadDrawings(pdfSheet); return pdfSheet; @@ -555,6 +576,11 @@ private static void MarkPrintTitleCells(PdfWorksheet pdfSheet, PdfRange range) } } + private void GetAdditionalContent(PdfPageSettings pageSettings, PdfWorksheet pdfSheet) + { + pdfSheet.AdditionalContent = string.IsNullOrEmpty(pageSettings.AdditionalContent) ? null : new PdfAdditionalContent(pageSettings, _dictionaries, pdfSheet.Worksheet); + } + private void GetHeaderFooter(PdfPageSettings pageSettings, PdfWorksheet pdfSheet) { pdfSheet.HeaderFooters = new PdfHeaderFooterCollection(pageSettings, _dictionaries, pdfSheet, pdfSheet.Worksheet.HeaderFooter); diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 9d2d9d3c5..90e6b728a 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -154,7 +154,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var sourceMap = (map.TextLines != null && map.TextLines.Count > 0) ? map : (map.Main != null && map.Main.TextLines != null && map.Main.TextLines.Count > 0) ? map.Main : null; if (sourceMap != null) { - var text = new PdfCellContentLayout(pageSettings, dictionaries, sourceMap, info, info.X, info.Y, info.Width, info.Height); + var text = new PdfCellContentLayout(pageSettings, dictionaries, sourceMap, info.X, info.Y, info.Width, info.Height); text.Name = map.Name; text.GidsAndCharMap(dictionaries); text.SetupClipping(info.X, info.Y, info.Width, info.Height); @@ -200,7 +200,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio //Text if (map.TextLines != null && map.TextLines.Count > 0) { - var text = new PdfCellContentLayout(pageSettings, dictionaries, map, info, x, y, effectiveWidth, rowHeight); + var text = new PdfCellContentLayout(pageSettings, dictionaries, map, x, y, effectiveWidth, rowHeight); text.Name = map.Name; text.GidsAndCharMap(dictionaries); if (NeedsClipping(map, pages[j], row, col)) @@ -397,6 +397,25 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio } } } + if (page.AddedContent != null && page.AddedContent.content?.TextLines != null && page.AddedContent.content.TextLines.Count > 0) + { + var content = page.AddedContent.content; + int last = content.TextLines.Count - 1; + var descent = content.TextLines[last].LargestDescent; + var ascent = content.TextLines[last].LargestAscent; + var sx = 10d; + var sy = descent + 2d; + var width = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; + var cellcont = new PdfCellContentLayout(pageSettings, dictionaries, content, sx, sy, width, 0); + cellcont.IsAddedContent = true; + cellcont.GidsAndCharMap(dictionaries); + pageLayout.AddChild(cellcont); + sy = pageSettings.PageSize.HeightPu - (descent + ascent + 4d); + cellcont = new PdfCellContentLayout(pageSettings, dictionaries, content, sx, sy, width, 0); + cellcont.IsAddedContent = true; + cellcont.GidsAndCharMap(dictionaries); + pageLayout.AddChild(cellcont); + } PdfGridlinesLayout.AddGridLines(pageSettings, pages[j], pageLayout, borderOnly: !pageSettings.ShowGridLines || pdfPages[i].IsCommentsPage); pageLayout.ChildObjects.Sort((a, b) => { @@ -623,7 +642,7 @@ private static void AddHeadingCell(PdfPageSettings pageSettings, PdfDictionaries if (cell.TextLines != null && cell.TextLines.Count > 0) { var info = new MergedCellDrawInfo { X = x, Y = y, Width = width, Height = height }; - var text = new PdfCellContentLayout(pageSettings, dictionaries, cell, info, x, y, width, height); + var text = new PdfCellContentLayout(pageSettings, dictionaries, cell, x, y, width, height); text.Name = namePrefix + "_Text"; text.IsHeading = true; text.GidsAndCharMap(dictionaries); @@ -727,7 +746,7 @@ private static void AddSpillCells(PdfPageSettings pageSettings, PdfDictionaries if (map.TextLines == null || map.TextLines.Count == 0) continue; if (s.ClipWidth <= 0d || s.ClipHeight <= 0d) continue; - var text = new PdfCellContentLayout(pageSettings, dictionaries, map, new MergedCellDrawInfo(), s.X, s.Y, s.Width, s.Height); + var text = new PdfCellContentLayout(pageSettings, dictionaries, map, s.X, s.Y, s.Width, s.Height); text.Name = "Spill_" + map.Name; text.IsPrintTitle = s.IsPrintTitle; text.GidsAndCharMap(dictionaries); @@ -753,7 +772,7 @@ private static void AddPrintTitleCells(PdfPageSettings pageSettings, PdfDictiona if (map.TextLines != null && map.TextLines.Count > 0) { var info = new MergedCellDrawInfo { X = t.X, Y = t.Y, Width = t.Width, Height = t.Height }; - var text = new PdfCellContentLayout(pageSettings, dictionaries, map, info, t.X, t.Y, t.Width, t.Height); + var text = new PdfCellContentLayout(pageSettings, dictionaries, map, t.X, t.Y, t.Width, t.Height); text.Name = map.Name; text.IsPrintTitle = true; text.GidsAndCharMap(dictionaries); @@ -938,6 +957,7 @@ internal static List GetPages(PdfPageSettings[] sheetSettings, PdfWorkshe pages = AssignRangeToPages(pageSettings, range, pages); pages = MapPage(range, pages); pages = GetHeaderFooter(range, pages, pdfSheet); + pages = GetAdditionalContent(pages, pdfSheet); pages = PrecomputeMergedCells(pageSettings, range, pages); pages = PrecomputeSpillCells(pageSettings, range, pages); pages = PrecomputePrintTitleCells(pageSettings, pdfSheet, range, pages); @@ -957,6 +977,7 @@ internal static List GetPages(PdfPageSettings[] sheetSettings, PdfWorkshe pages = AssignRangeToPages(pageSettings, pdfSheet.CommentsAndNotes, pages); pages = MapPage(pdfSheet.CommentsAndNotes, pages); pages = GetHeaderFooter(pdfSheet.CommentsAndNotes, pages, pdfSheet); + pages = GetAdditionalContent(pages, pdfSheet); pageSettings.ShowHeadings = savedShowHeadings; pages.IsCommentsPage = true; pages.Settings = pageSettings; @@ -1701,6 +1722,17 @@ private static Pages GetHeaderFooter(PdfRange range, Pages pdfPages, PdfWorkshee return pdfPages; } + private static Pages GetAdditionalContent(Pages pdfPages, PdfWorksheet pdfSheet) + { + for (int i = 0; i < pdfPages.Page.Length; i++) + { + var page = pdfPages.Page[i]; + page.AddedContent = pdfSheet.AdditionalContent; + pdfPages.Page[i] = page; + } + return pdfPages; + } + private static void SubstitutePageNumbers(PdfPageSettings pageSettings, PdfDictionaries dictionaries, PdfHeaderFooter hf, int pageNumber, int totalPages) { if (hf == null) return; diff --git a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs index 1e38fa97b..cb93622c7 100644 --- a/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs +++ b/src/EPPlus/Export/PdfExport/Settings/GetPdfSettings.cs @@ -21,6 +21,7 @@ internal static class GetPdfSettings { internal static PdfPageSettings GetPdfSettingsFromPrinterSettings(ExcelWorkbook workbook, ExcelPrinterSettings eps) { + bool stampNonCommercial = ExcelPackage.License.LicenseType == EPPlusLicenseType.NonCommercialOrganization; var settings = new PdfPageSettings(workbook.RenderContext.FontEngine); ApplyPrinterSettings(settings, eps); return settings; diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfAdditionalContent.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfAdditionalContent.cs new file mode 100644 index 000000000..083a6ae4a --- /dev/null +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfAdditionalContent.cs @@ -0,0 +1,93 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 27/11/2025 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ +using EPPlus.Export.Pdf.Layout; +using EPPlus.Export.Pdf.Resources; +using EPPlus.Export.Pdf.Settings; +using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.Integration.DataHolders; +using OfficeOpenXml.Interfaces.Fonts; +using System.Collections.Generic; +using System.Drawing; + +namespace OfficeOpenXml.Export.PdfExport.TextMapping +{ + internal class PdfAdditionalContent + { + internal static readonly Color TextColor = Color.Gray; + internal const float FontSize = 7f; + internal PdfCellBase content; + + public PdfAdditionalContent(PdfPageSettings pageSettings, PdfDictionaries dictionaries, ExcelWorksheet ws) + { + if (string.IsNullOrEmpty(pageSettings.AdditionalContent)) + { + return; + } + else + { + var ns = ws.Workbook.Styles.GetNormalStyle(); + var fragment = new TextFragment + { + Font = new RichTextFormatSimple + { + Family = ns.Style.Font.Name, + Size = FontSize, + SubFamily = FontSubFamily.Regular, + }, + Text = ComposeAdditionalContent(), + }; + fragment.RichTextOptions.UnderlineType = 12; + fragment.RichTextOptions.StrikeType = 1; + fragment.RichTextOptions.FontColor = TextColor; + content = new PdfCellBase + { + TextFragments = new List { fragment }, + ContentAligmnet = new PdfCellAlignmentData + { + HorizontalAlignment = EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment.Left, + VerticalAlignment = EPPlus.Export.Pdf.Enums.ExcelVerticalAlignment.Bottom, + }, + }; + dictionaries.AddFont(pageSettings, fragment.Font.Family, fragment.Font.SubFamily, fragment.Text); + } + } + + private static string ComposeAdditionalContent() + { + byte[] d = + { + 0xF3, 0xAE, 0x8C, 0x77, 0x03, 0x12, 0x25, 0xC6, 0xBF, 0xC9, 0xBC, 0x8F, + 0x3B, 0x5F, 0x21, 0x08, 0xF8, 0xC4, 0xA1, 0x91, 0x77, 0x12, 0x24, 0x03, + 0xE6, 0xC0, 0xAA, 0xCC, 0x4E, 0x7A, 0x19, 0x04, 0xF2, 0xD5, 0xE5, 0x91, + 0x6D, 0x46, 0x24, 0x12, 0x5F, 0xFF, 0x9D, 0xB2, 0x94, 0x74, 0x14, 0x3B, + 0x18, 0xFB, 0xD8, 0xB1, 0x81, 0x71, 0x58, 0x31, 0x03, 0xAE, 0xC1, 0xA5, + 0x88, 0x6F, 0x47, 0x3B, 0x02, 0xA6, 0xC3, 0xAB, 0x91, 0x22, 0x51, 0x25, + 0x2D, 0x0D, 0xF2, 0xD2, 0xBA, 0x96, 0x39, 0x57, 0x25, 0x56, 0xFB, 0xDB, + 0xBD, 0xDF, 0x72, 0x5F, 0x22, 0x03, 0xE8, 0xDE, 0xA8, 0x83, 0x68, 0x44, + 0x67, 0x13, 0xF6, 0xC1, 0xED, 0xC2, 0x55, 0x4F, 0x1F, 0x2C, 0x18, 0xF1, + 0xD4, 0xAC, 0x9C, 0x38, 0x43, 0x3E, 0x1C, 0xE7, 0x93, 0xBC, 0x9E, 0x64, + 0x46, 0x2D, 0x08, 0xA0, 0x8B, 0xBC, 0x80, 0x7B, 0x4E, 0x32, 0x45, 0xEC, + 0xD7, 0xB6, 0x91, 0x73, 0x25, 0x11, 0x72, 0x0B, 0xEC, 0xCD, 0xF7, 0x9D, + 0x67, 0x46, 0x39, 0x01, 0xE0, 0xC1, 0xBE, 0x96, 0x7B, 0x59, 0x2C, 0x1E, + 0xEE, 0x84, 0xAA, 0x87, 0x6A, 0x09, 0x65, 0x10, 0xEC, 0x82, 0xB1, 0x95, + 0x8D, 0x7D, 0x5C, 0x2F, 0x1E, 0xBA, 0xD8, 0xF8, 0x9B, 0x7F, 0x56, 0x31, + 0x1D, 0xE1, 0xD4, 0xFE, + }; + const int a = 0xA7, m = 31; + var buf = new char[d.Length]; + for (int i = 0; i < d.Length; i++) + buf[i] = (char)(d[i] ^ ((a + i * m) & 0xFF)); + return new string(buf); + } + } +} From a9e363ddaf1863b08500e8795ac00f58da7f491d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 7 Oct 2026 15:28:49 +0200 Subject: [PATCH 08/17] removed debug comments --- src/EPPlus.Export.Pdf/ExcelPdf.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index ec7d608cd..4616a1b06 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -191,7 +191,6 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) var headingLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout cl && cl.IsHeading) || (t is PdfCellContentLayout ccl && ccl.IsHeading)); var printTitleLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout pl && pl.IsPrintTitle) || (t is PdfCellContentLayout pcl && pcl.IsPrintTitle) || (t is PdfCellBorderLayout pbl && pbl.IsPrintTitle)); var contentStream = new PdfContentStream(_document.Count + 1); - contentStream.AddCommand($"% {pageLayout.Name} start"); //Start page content clipping rectangle. contentStream.AddCommand("q"); contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); @@ -202,17 +201,14 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) } foreach (PdfCellLayout fill in cells.OfType()) { - contentStream.AddCommand($"% CELL FILL : {fill.Name}"); contentStream.AddCellLayout(fill, GetPatternLabel(fill)); } foreach (PdfCellContentLayout content in cells.OfType()) { - contentStream.AddCommand($"% CELL TEXT : {content.Name}"); contentStream.AddCellContentLayout(content, _dictionaries, pageSettings); } foreach (PdfCellBorderLayout border in cells.OfType()) { - contentStream.AddCommand($"% CELL BORDER : {border.Name}"); contentStream.AddBorderLayout(border); } foreach (PdfImageLayout image in pageLayout.ChildObjects.OfType()) @@ -224,13 +220,11 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) } //Close the clipping rectangle. contentStream.AddCommand("Q"); - contentStream.AddCommand($"% Margin Clip End"); //Add headings contentStream.AddCommand("q"); contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); foreach (var heading in headingLayouts) { - contentStream.AddCommand($"% HEADING : {heading.Name}"); switch (heading) { case PdfCellLayout layout: @@ -269,7 +263,6 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); foreach (var titleCell in printTitleLayouts) { - contentStream.AddCommand($"% PRINT TITLE : {titleCell.Name}"); switch (titleCell) { case PdfCellLayout layout: @@ -280,14 +273,13 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddBorderLayout(borderLayout); break; } } + contentStream.AddCommand("Q"); foreach (var content in AdditionalContent) { contentStream.AddCellContentLayout(content, _dictionaries, pageSettings); } - contentStream.AddCommand("Q"); _document.Add(contentStream); page.contentObjectNumbers.Add(contentStream.objectNumber); - contentStream.AddCommand($"% {pageLayout.Name} end"); } //Add Info From 912d43951afc16ddecc7da0ad5e90fd7c6704325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 7 Oct 2026 15:32:12 +0200 Subject: [PATCH 09/17] removed more comments from pdf. --- .../DocumentObjects/PdfBorderRenderer.cs | 5 ----- .../DocumentObjects/PdfContentStream.cs | 19 ------------------- 2 files changed, 24 deletions(-) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfBorderRenderer.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfBorderRenderer.cs index cab8e0dfc..7031399f4 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfBorderRenderer.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfBorderRenderer.cs @@ -51,7 +51,6 @@ public PdfBorderRenderer(PdfCellBorderLayout cell) public void RenderBorder(PdfContentStream contentStream) { - contentStream.AddCommand($"% Border Start: {Name}"); contentStream.AddCommand("q"); if (Top != null) RenderBorder(contentStream, Top); if (Bottom != null) RenderBorder(contentStream, Bottom); @@ -60,12 +59,10 @@ public void RenderBorder(PdfContentStream contentStream) if (DiagonalUp != null) RenderBorder(contentStream, DiagonalUp); if (DiagonalDown != null) RenderBorder(contentStream, DiagonalDown); contentStream.AddCommand("Q"); - contentStream.AddCommand($"% Border End: {Name}"); } private void RenderBorder(PdfContentStream contentStream, PdfCellBorderData border) { - contentStream.AddCommand($"% Border Type Start: {border.LineType.ToString()}"); double x1 = X, y1 = Y, x2 = X, y2 = Y; switch (border.LineType) { @@ -132,7 +129,6 @@ private void RenderBorder(PdfContentStream contentStream, PdfCellBorderData bord contentStream.AddCommand($"{x1.ToPdfStringF4()} {y1.ToPdfStringF4()} m"); contentStream.AddCommand($"{x2.ToPdfStringF4()} {y2.ToPdfStringF4()} l"); contentStream.AddCommand("S"); - contentStream.AddCommand($"% Border Type End: {border.LineType.ToString()}"); return; } switch (border.BorderStyle) @@ -182,7 +178,6 @@ private void RenderBorder(PdfContentStream contentStream, PdfCellBorderData bord contentStream.AddCommand($"{x1.ToPdfStringF4()} {y1.ToPdfStringF4()} m"); contentStream.AddCommand($"{x2.ToPdfStringF4()} {y2.ToPdfStringF4()} l"); contentStream.AddCommand("S"); - contentStream.AddCommand($"% Border Type End: {border.LineType.ToString()}"); } private void DrawBasicBorder(PdfContentStream contentStream, PdfCellBorderData border, double width, string dash) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs index 9b12b8912..1c8d972c9 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs @@ -60,18 +60,15 @@ public void AddCellLayout(PdfCellLayout cell, string label) if (cell.Size.X <= 0d || cell.Size.Y <= 0d) return; if (cell.CellFillData.GradientFillData != null && cell.CellFillData.PatternStyle != ExcelFillStyle.Solid) { - commands.Add($"% Pattern Start: {cell.Name}"); commands.Add("q"); commands.Add("/Pattern cs"); commands.Add($"/{label} scn"); commands.Add($"{cell.LocalPosition.X.ToPdfString()} {cell.LocalPosition.Y.ToPdfString()} {cell.Size.X.ToPdfString()} {cell.Size.Y.ToPdfString()} re"); commands.Add("f"); commands.Add("Q"); - commands.Add($"% Pattern End: {cell.Name}"); } else if (cell.CellFillData.BackgroundColor != Color.Empty && cell.CellFillData.PatternStyle == ExcelFillStyle.Solid) { - commands.Add($"% Solid Fill Start: {cell.Name}"); commands.Add("q"); commands.Add($"{GridLine.HalfWidth.ToPdfString()} w"); commands.Add(cell.CellFillData.BackgroundColor.ToFillCommand()); @@ -79,11 +76,9 @@ public void AddCellLayout(PdfCellLayout cell, string label) commands.Add($"{cell.LocalPosition.X.ToPdfString()} {cell.LocalPosition.Y.ToPdfString()} {cell.Size.X.ToPdfString()} {cell.Size.Y.ToPdfString()} re"); commands.Add("B"); commands.Add("Q"); - commands.Add($"% Solid Fill End: {cell.Name}"); } else if (cell.CellFillData.PatternStyle != ExcelFillStyle.None) { - commands.Add($"% Pattern Start: {cell.Name}"); if (cell.CellFillData.BackgroundColor != Color.Empty) { commands.Add("q"); @@ -94,14 +89,12 @@ public void AddCellLayout(PdfCellLayout cell, string label) commands.Add("B"); commands.Add("Q"); } - commands.Add("q"); commands.Add("/Pattern cs"); commands.Add($"/{label} scn"); commands.Add($"{cell.LocalPosition.X.ToPdfString()} {cell.LocalPosition.Y.ToPdfString()} {cell.Size.X.ToPdfString()} {cell.Size.Y.ToPdfString()} re"); commands.Add("f"); commands.Add("Q"); - commands.Add($"% Pattern End: {cell.Name}"); } } @@ -372,12 +365,10 @@ private static ushort GetUnitsPerEm(ushort[] fontUnitsPerEm, byte fontId, ushort public void AddCellContentLayout(PdfCellContentLayout cell, PdfDictionaries dictionaries, PdfPageSettings pageSettings) { - commands.Add($"% Content Start: {cell.Name}"); commands.Add("q"); if (cell.Clip) AddClipping(cell); AddText(cell, cell.LocalPosition, cell.CellAlignmentData.TextRotation, dictionaries, pageSettings); commands.Add("Q"); - commands.Add($"% Content End: {cell.Name}"); } private void AddClipping(PdfCellContentLayout cell) @@ -389,8 +380,6 @@ public void AddInnerGridLines(Transform pageLayout) { if (pageLayout is not PdfPageLayout pl) return; if (pl.isCommentsPage) return; - - commands.Add($"% Gridlines Start"); commands.Add("q"); commands.Add($"{GridLine.Width.ToPdfString()} w"); commands.Add(Color.Black.ToFillCommand()); @@ -413,24 +402,20 @@ public void AddInnerGridLines(Transform pageLayout) } commands.Add("f"); commands.Add("Q"); - commands.Add($"% Gridlines End"); } public void AddImage(string label, double x, double y, double width, double height) { - commands.Add($"% Image Start: {label}"); commands.Add("q"); commands.Add($"{width.ToPdfString()} 0 0 {height.ToPdfString()} {x.ToPdfString()} {y.ToPdfString()} cm"); commands.Add($"/{label} Do"); commands.Add("Q"); - commands.Add($"% Image End: {label}"); } public void AddPrintTitleGridLines(Transform pageLayout) { if (pageLayout is not PdfPageLayout pl) return; if (pl.isCommentsPage) return; - commands.Add($"% Print Title Gridlines Start"); commands.Add("q"); commands.Add($"{GridLine.Width.ToPdfString()} w"); commands.Add(Color.Black.ToFillCommand()); @@ -453,7 +438,6 @@ public void AddPrintTitleGridLines(Transform pageLayout) } commands.Add("f"); commands.Add("Q"); - commands.Add($"% Print Title Gridlines End"); } public void AddOuterGridBorder(Transform pageLayout) @@ -461,7 +445,6 @@ public void AddOuterGridBorder(Transform pageLayout) if (pageLayout is not PdfPageLayout pl) return; if (pl.isCommentsPage) return; - commands.Add($"% Gridlines Border Start"); commands.Add("q"); commands.Add("1.0 w"); commands.Add("2 J"); @@ -474,14 +457,12 @@ public void AddOuterGridBorder(Transform pageLayout) } commands.Add("S"); commands.Add("Q"); - commands.Add($"% Gridlines Border End"); } public void AddMarginClipping(PdfPageLayout pageLayout, PdfPageSettings pageSettings) { if (pageLayout is not PdfPageLayout pl) return; if (pageLayout.isCommentsPage) return; - commands.Add($"% Margin Clip Start"); if (pl.BorderLines.Count == 0) return; double top = double.MinValue; double bottom = double.MaxValue; From 5dd4d7926f26b429de7d0715075e67cc58209cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Thu, 8 Oct 2026 11:03:06 +0200 Subject: [PATCH 10/17] Added additional info to info object. --- .../DocumentObjects/PdfInfoObject.cs | 31 ++++++++++++------- src/EPPlus.Export.Pdf/ExcelPdf.cs | 4 +-- .../Settings/PdfDocumentSettings.cs | 8 +++++ .../Settings/PdfPageSettings.cs | 5 +++ .../Export/PdfExport/EPPlusToPdfWriter.cs | 28 +++++++++++++++++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfInfoObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfInfoObject.cs index 4937c84fb..077f60f8c 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfInfoObject.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfInfoObject.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Settings; using System; using System.IO; using System.Text; @@ -18,11 +19,17 @@ namespace EPPlus.Export.Pdf.DocumentObjects { internal class PdfInfoObject : PdfObject { - public string Title; + internal string Title; + internal string LicenseType; + internal string LicenseHolder; + internal string Author; - public PdfInfoObject(int objectNumber, string Title, int version = 0) : base(objectNumber, version) + public PdfInfoObject(int objectNumber, PdfDocumentSettings documentSettings, int version = 0) : base(objectNumber, version) { - this.Title = Title; + Title = documentSettings.Title; + Author = documentSettings.Author; + LicenseType = documentSettings.LicenseType; + LicenseHolder = documentSettings.LicenseHolder; } internal override string RenderDictionary() @@ -34,10 +41,10 @@ internal override string RenderDictionary() string pdfDate = string.Format("D:{0:yyyyMMddHHmmss}{1}{2:00}'{3:00}'", now, sign, offset.Hours, offset.Minutes); var sb = new StringBuilder(); sb.AppendFormat($"<< /Title ({Title})\n" + - $" /Author (EPPlus)\n" + - $" /Subject (EPPlus PDF Export)\n" + - $" /Keywords (EPPlus, EPPlus Software)" + - $" /Creator (EPPlus Software)\n" + + $" /Author ({Author})\n" + + $" /Subject (EPPlus PDF Export with {LicenseType} for {LicenseHolder})\n" + + $" /Keywords (EPPlus, EPPlus Software, PDF, Export, {LicenseType}, {LicenseHolder})" + + $" /Creator (EPPlus Software {LicenseType}, {LicenseHolder})\n" + $" /Producer (EPPlus Software PDF Exporter)\n" + $" /CreationDate ({pdfDate})\n" + $" /ModDate ({pdfDate})\n" + @@ -53,14 +60,14 @@ internal override void RenderDictionary(BinaryWriter bw) offset = offset.Duration(); string pdfDate = string.Format("D:{0:yyyyMMddHHmmss}{1}{2:00}'{3:00}'", now, sign, offset.Hours, offset.Minutes); WriteAscii(bw, $"<< /Title ({Title})\n" + - $" /Author (EPPlus)\n" + - $" /Subject (EPPlus PDF Export)\n" + - $" /Keywords (EPPlus, EPPlus Software)\n" + - $" /Creator (EPPlus Software)\n" + + $" /Author ({Author})\n" + + $" /Subject (EPPlus PDF Export with {LicenseType} for {LicenseHolder})\n" + + $" /Keywords (EPPlus, EPPlus Software, PDF, Export, {LicenseType}, {LicenseHolder})" + + $" /Creator (EPPlus Software {LicenseType}, {LicenseHolder})\n" + $" /Producer (EPPlus Software PDF Exporter)\n" + $" /CreationDate ({pdfDate})\n" + $" /ModDate ({pdfDate})\n" + $" /Trapped /False >>"); } } -} +} \ No newline at end of file diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 4616a1b06..3ec3f38ac 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -283,9 +283,9 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) } //Add Info - private PdfInfoObject AddInfoObject(string workBookName = "") + private PdfInfoObject AddInfoObject() { - var info = new PdfInfoObject(_document.Count + 1, workBookName); + var info = new PdfInfoObject(_document.Count + 1, _documentSettings); _document.Add(info); return info; } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs index 490ae39dd..45485e53a 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs @@ -25,6 +25,10 @@ internal class PdfDocumentSettings internal int FirstPageNumber; internal bool Debug; internal bool PrintAsText; + internal string LicenseType; + internal string LicenseHolder; + internal string Title; + internal string Author; internal static PdfDocumentSettings From(PdfPageSettings s) { @@ -38,6 +42,10 @@ internal static PdfDocumentSettings From(PdfPageSettings s) FirstPageNumber = s.FirstPageNumber, Debug = s.Debug, PrintAsText = s.PrintAsText, + LicenseType = s.LicenseType, + LicenseHolder = s.LicenseHolder, + Title = s.Title, + Author = s.Author, }; } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index fe96ac403..6f182a9b1 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -22,6 +22,11 @@ namespace EPPlus.Export.Pdf.Settings /// public class PdfPageSettings { + internal string Author { get; set; } + internal string Title { get; set; } + internal string LicenseType { get; set; } + internal string LicenseHolder { get; set; } + private OpenTypeFontEngine _fontEngine; /// /// Get the current font engine that is being used by EPPlus. diff --git a/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs index c54d316b7..f34ef43af 100644 --- a/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs +++ b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs @@ -138,6 +138,7 @@ private static string AddAdditionalContent() private void BuildPdf(PdfPageSettings pageSettings, ExcelWorksheet worksheet, Action writePdf) { + GetLicenseInfo(pageSettings, worksheet.Workbook); pageSettings.defaultFontName = worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet pdfSheet = null; @@ -183,6 +184,7 @@ public EPPlusToPdfWriter(PdfPageSettings pageSettings, ExcelRangeBase range) private void BuildPdfFromRange(PdfPageSettings pageSettings, ExcelRangeBase range, Action writePdf) { + GetLicenseInfo(pageSettings, range.Worksheet.Workbook); pageSettings.defaultFontName = range.Worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet pdfSheet = null; @@ -218,6 +220,7 @@ public EPPlusToPdfWriter(PdfPageSettings pageSettings, List rang private void HandleRangeCollection(PdfPageSettings pageSettings, ExcelRangeBase[] ranges, Action writePdf) { + GetLicenseInfo(pageSettings, ranges[0].Worksheet.Workbook); pageSettings.defaultFontName = ranges[0].Worksheet.Workbook.ThemeManager.GetOrCreateTheme().FontScheme.MinorFont[0].Typeface; pageSettings.AdditionalContent = AddAdditionalContent(); PdfWorksheet[] pdfSheets = null; @@ -289,6 +292,31 @@ internal PdfCellCollection GetCellCollectionFromRange(PdfPageSettings pageSettin //Private Methods + private void GetLicenseInfo(PdfPageSettings pageSettings, ExcelWorkbook workbook) + { + pageSettings.Title = string.IsNullOrEmpty(workbook.Properties.Title) + ? string.IsNullOrEmpty(workbook._package.File.Name) + ? "Untitled EPPlus Workbook" + : workbook._package.File.Name + : workbook.Properties.Title; + + pageSettings.Author = string.IsNullOrEmpty(workbook.Properties.Author) + ? string.IsNullOrEmpty(ExcelPackage.License.LegalName) + ? "Unnamed Author using EPPLus" + : ExcelPackage.License.LegalName + : workbook.Properties.Author; + + pageSettings.LicenseType = IsFullEdition + ? "Commercial License" + : ExcelPackage.License.LicenseType == EPPlusLicenseType.NonCommercialOrganization + ? "Non-Commercial License for Organization" + : "Personal Non-Commercial License"; + + pageSettings.LicenseHolder = string.IsNullOrEmpty(ExcelPackage.License.LegalName) + ? "Unnamed License holder using EPPLus" + : ExcelPackage.License.LegalName; + } + private Action WriteToFile(PdfPageSettings pageSettings, string fileName) { return layout => new ExcelPdf().CreatePdf(PdfDocumentSettings.From(pageSettings), _dictionaries, layout, fileName); From d7cac71e77d4d11f8a8be796b2d59a4f1846bda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Thu, 8 Oct 2026 14:42:22 +0200 Subject: [PATCH 11/17] fixed null error when reading File from package. --- src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs index f34ef43af..8d55c8fe1 100644 --- a/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs +++ b/src/EPPlus/Export/PdfExport/EPPlusToPdfWriter.cs @@ -295,9 +295,11 @@ internal PdfCellCollection GetCellCollectionFromRange(PdfPageSettings pageSettin private void GetLicenseInfo(PdfPageSettings pageSettings, ExcelWorkbook workbook) { pageSettings.Title = string.IsNullOrEmpty(workbook.Properties.Title) - ? string.IsNullOrEmpty(workbook._package.File.Name) + ? workbook._package.File == null ? "Untitled EPPlus Workbook" - : workbook._package.File.Name + : string.IsNullOrEmpty(workbook._package.File.Name) + ? "Untitled EPPlus Workbook" + : workbook._package.File.Name : workbook.Properties.Title; pageSettings.Author = string.IsNullOrEmpty(workbook.Properties.Author) From cc62230bc60185a56d1dd8e00abaef0029b1a25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Mon, 12 Oct 2026 15:34:36 +0200 Subject: [PATCH 12/17] header footer scaling. --- src/EPPlus.Export.Pdf/ExcelPdf.cs | 60 +++++++++++++--- .../Layout/PdfCellContentLayout.cs | 2 + .../Layout/PdfHeaderFooterPart.cs | 24 +++++++ src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs | 2 + .../Settings/PdfPageSettings.cs | 2 + .../Export/PdfExport/Layout/PdfLayout.cs | 72 +++++++++++++------ 6 files changed, 129 insertions(+), 33 deletions(-) create mode 100644 src/EPPlus.Export.Pdf/Layout/PdfHeaderFooterPart.cs diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 3ec3f38ac..9e7064707 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -186,7 +186,7 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) !(t is PdfCellLayout cc && (cc.IsHeading || cc.IsPrintTitle)) && !(t is PdfCellContentLayout ccl && (ccl.IsHeaderFooter || ccl.IsHeading || ccl.IsPrintTitle || ccl.IsAddedContent)) && !(t is PdfCellBorderLayout cbl && cbl.IsPrintTitle)).ToList(); - var headerFooterLayouts = pageLayout.ChildObjects.OfType().Where(t => t.IsHeaderFooter); + //var headerFooterLayouts = pageLayout.ChildObjects.OfType().Where(t => t.IsHeaderFooter); var AdditionalContent = pageLayout.ChildObjects.OfType().Where(t => t.IsAddedContent && !(pageSettings.AdditionalContent != "null")); var headingLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout cl && cl.IsHeading) || (t is PdfCellContentLayout ccl && ccl.IsHeading)); var printTitleLayouts = pageLayout.ChildObjects.Where(t => (t is PdfCellLayout pl && pl.IsPrintTitle) || (t is PdfCellContentLayout pcl && pcl.IsPrintTitle) || (t is PdfCellBorderLayout pbl && pbl.IsPrintTitle)); @@ -246,18 +246,56 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddCommand("Q"); } //Add header and footer. - foreach (var hf in headerFooterLayouts) + //foreach (var hf in headerFooterLayouts) + //{ + // if (pageLayout.ScaleHeaderFooterWithDocument) + // { + // contentStream.AddCommand("q"); + // contentStream.AddContentScale(contentScale, hf.ScaleAnchorX, hf.ScaleAnchorY); + // } + // contentStream.AddCellContentLayout(hf, _dictionaries, pageSettings); + // if (pageLayout.ScaleHeaderFooterWithDocument) + // { + // contentStream.AddCommand("Q"); + // } + //} + ////Add images + //foreach (PdfImageLayout image in pageLayout.ChildObjects.OfType()) + //{ + // if (!image.IsHeaderFooter) continue; + // var imageResource = _dictionaries.AddImage(image.ImageBytes); + // contentStream.AddImage(imageResource.Label, image.LocalPosition.X, image.LocalPosition.Y, image.Size.X, image.Size.Y); + // if (PdfImageXObject.ProducesSoftMask(image.ImageBytes)) page.HasTransparency = true; + //} + //Add header and footer — each part scaled about its own pivot + foreach (var part in pageLayout.HeaderFooterParts) { - contentStream.AddCellContentLayout(hf, _dictionaries, pageSettings); - } - //Add images - foreach (PdfImageLayout image in pageLayout.ChildObjects.OfType()) - { - if (!image.IsHeaderFooter) continue; - var imageResource = _dictionaries.AddImage(image.ImageBytes); - contentStream.AddImage(imageResource.Label, image.LocalPosition.X, image.LocalPosition.Y, image.Size.X, image.Size.Y); - if (PdfImageXObject.ProducesSoftMask(image.ImageBytes)) page.HasTransparency = true; + bool scaleHf = pageLayout.ScaleHeaderFooterWithDocument; + if (scaleHf) + { + contentStream.AddCommand("q"); + contentStream.AddContentScale(contentScale, part.ScalePivot.X, part.ScalePivot.Y); + } + + foreach (var item in part.Items) + { + switch (item) + { + case PdfCellContentLayout text: + contentStream.AddCellContentLayout(text, _dictionaries, pageSettings); + break; + case PdfImageLayout image: + var imageResource = _dictionaries.AddImage(image.ImageBytes); + contentStream.AddImage(imageResource.Label, image.LocalPosition.X, image.LocalPosition.Y, image.Size.X, image.Size.Y); + if (PdfImageXObject.ProducesSoftMask(image.ImageBytes)) page.HasTransparency = true; + break; + } + } + + if (scaleHf) + contentStream.AddCommand("Q"); } + //Add print titles contentStream.AddCommand("q"); contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); diff --git a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs index 1005c3b5d..908fe7368 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs @@ -36,6 +36,8 @@ internal class PdfCellContentLayout : Transform public TextLineCollection TextLines; public double LeftTextSpillLength = 0d; public double RightTextSpillLength = 0d; + public double ScaleAnchorX; + public double ScaleAnchorY; private double bottomMargin = 3.5d; //Guessed number private double rightMargin = 1.4d; //I guessed this one too.. diff --git a/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooterPart.cs b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooterPart.cs new file mode 100644 index 000000000..19d3da079 --- /dev/null +++ b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooterPart.cs @@ -0,0 +1,24 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 27/11/2025 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ +using EPPlus.Graphics; +using EPPlus.Graphics.Geometry; +using System.Collections.Generic; + +namespace EPPlus.Export.Pdf.Layout +{ + internal sealed class PdfHeaderFooterPart + { + public Vector2 ScalePivot; + public readonly List Items = new List(); + } +} diff --git a/src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs index 814ac2c49..b9f07f9ce 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs @@ -28,6 +28,8 @@ internal class PdfPageLayout : Transform public double PrintTitleWidth; public double PrintTitleHeight; public bool isCommentsPage = false; + public bool ScaleHeaderFooterWithDocument; + internal List HeaderFooterParts = new List(); internal PdfPageSettings Settings; public PdfPageLayout(double x, double y, double width, double height) : base(x, y, width, height) diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index 6f182a9b1..128297a05 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -203,6 +203,8 @@ public PdfScaling Scaling private double _resolvedFitScale = 1d; internal void SetResolvedFitScale(double s) { _resolvedFitScale = s; } + internal bool ScaleHeaderWithDocument { get; set; } + internal double ContentScale { get diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 90e6b728a..2eec3baa5 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -19,6 +19,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; +using EPPlus.Graphics.Geometry; using EPPlus.Graphics.Units; using OfficeOpenXml.Drawing; using OfficeOpenXml.Export.PdfExport.Data; @@ -244,6 +245,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio } if (page.HeaderFooters != null) { + pageLayout.ScaleHeaderFooterWithDocument = page.HeaderFooters.ScaleWithDocument; var hfType = page.HeaderFooters.GetPageType(physicalPageIndex); var leftH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Left); if (leftH != null && !leftH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, leftH, displayedPageNumber, totalPages)) @@ -252,10 +254,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var hfx = pageSettings.Margins.LeftPu; var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; var text = new PdfCellContentLayout(pageSettings, dictionaries, leftH, hfx, hfy, 0, 0); + text.ScaleAnchorX = hfx; // left edge + text.ScaleAnchorY = hfy + ascent; text.Name = "LeftHeader"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Left, pageSettings); } var centerH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Center); if (centerH != null && !centerH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, centerH, displayedPageNumber, totalPages)) @@ -265,10 +267,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; var hfWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; var text = new PdfCellContentLayout(pageSettings, dictionaries, centerH, hfx, hfy, hfWidth, 0); + text.ScaleAnchorX = hfx + hfWidth / 2d; + text.ScaleAnchorY = hfy + ascent; text.Name = "CenterHeader"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Center, pageSettings); } var rightH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Right); if (rightH != null && !rightH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, rightH, displayedPageNumber, totalPages)) @@ -277,10 +279,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var hfx = pageSettings.PageSize.WidthPu - pageSettings.Margins.RightPu; var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; var text = new PdfCellContentLayout(pageSettings, dictionaries, rightH, hfx, hfy, 0, 0); + text.ScaleAnchorX = hfx; + text.ScaleAnchorY = hfy + ascent; text.Name = "RightHeader"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Right, pageSettings); } var leftF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Left); if (leftF != null && !leftF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, leftF, displayedPageNumber, totalPages)) @@ -290,10 +292,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var hfx = pageSettings.Margins.LeftPu; var hfy = pageSettings.Margins.FooterPu + descent; var text = new PdfCellContentLayout(pageSettings, dictionaries, leftF, hfx, hfy, 0, 0); + text.ScaleAnchorX = hfx; + text.ScaleAnchorY = hfy - descent; text.Name = "LeftFooter"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Left, pageSettings); } var centerF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Center); if (centerF != null && !centerF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, centerF, displayedPageNumber, totalPages)) @@ -302,11 +304,12 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var descent = centerF.Content.TextLines[last].LargestDescent; var hfx = pageSettings.PageSize.WidthPu / 2d; var hfy = pageSettings.Margins.FooterPu + descent; + var hfWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; var text = new PdfCellContentLayout(pageSettings, dictionaries, centerF, hfx, hfy, 0, 0); + text.ScaleAnchorX = hfx + hfWidth / 2d; + text.ScaleAnchorY = hfy - descent; text.Name = "CenterFooter"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Center, pageSettings); } var rightF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Right); if (rightF != null && !rightF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, rightF, displayedPageNumber, totalPages)) @@ -316,10 +319,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var hfx = pageSettings.PageSize.WidthPu - pageSettings.Margins.RightPu; var hfy = pageSettings.Margins.FooterPu + descent; var text = new PdfCellContentLayout(pageSettings, dictionaries, rightF, hfx, hfy, 0, 0); + text.ScaleAnchorX = hfx; + text.ScaleAnchorY = hfy - descent; text.Name = "RightFooter"; - text.IsHeaderFooter = true; - text.GidsAndCharMap(dictionaries); - pageLayout.AddChild(text); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Right, pageSettings); } double hfContentLeft = pageSettings.Margins.LeftPu; double hfContentWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; @@ -372,17 +375,17 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio ? pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent : pageSettings.Margins.FooterPu + descent; double textY = System.Math.Min(naturalBaseline, imgBottom + descent); - + var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(hfSection, hfAlign, pageSettings) }; double cursor = runStart; if (beforeRun != null) { - pageLayout.AddChild(PlaceHeaderFooterRun(pageSettings, dictionaries, beforeRun, cursor, textY)); + part.Items.Add(PlaceHeaderFooterRun(pageSettings, dictionaries, beforeRun, cursor, textY)); } cursor += beforeWidth; if (canEmbed) { - pageLayout.AddChild(new PdfImageLayout(cursor, imgTop, imgW, imgH) + part.Items.Add(new PdfImageLayout(cursor, imgTop, imgW, imgH) { ImageBytes = hf.ImageBytes, IsHeaderFooter = true, @@ -392,8 +395,10 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio cursor += imgW; if (afterRun != null) { - pageLayout.AddChild(PlaceHeaderFooterRun(pageSettings, dictionaries, afterRun, cursor, textY)); + part.Items.Add(PlaceHeaderFooterRun(pageSettings, dictionaries, afterRun, cursor, textY)); } + if (part.Items.Count > 0) + pageLayout.HeaderFooterParts.Add(part); } } } @@ -1746,6 +1751,29 @@ private static bool ShapeHeaderFooterText(PdfPageSettings pageSettings, PdfDicti return hf.Content != null && hf.Content.TextLines != null && hf.Content.TextLines.Count > 0; } + private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps) + { + double left = ps.Margins.LeftPu; + double width = ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu; + double x = align == HeaderFooterAlignment.Left ? left + : align == HeaderFooterAlignment.Right ? left + width + : left + width / 2d; + double y = section == HeaderFooterSection.Header + ? ps.PageSize.HeightPu - ps.Margins.HeaderPu + : ps.Margins.FooterPu; + return new Vector2(x, y); + } + + private static void AddHeaderFooterTextPart(PdfPageLayout pageLayout, PdfDictionaries dictionaries, + PdfCellContentLayout text, HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps) + { + text.IsHeaderFooter = true; + text.GidsAndCharMap(dictionaries); + var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(section, align, ps) }; + part.Items.Add(text); + pageLayout.HeaderFooterParts.Add(part); + } + private static void ApplyPageNumbers(PdfHeaderFooter hf, int pageNumber, int totalPages) { if (hf == null) return; From 2ed20f9bf3f4302639f9e95f04e485d13daa3471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Mon, 12 Oct 2026 16:31:31 +0200 Subject: [PATCH 13/17] align with page margins progress --- .../Export/PdfExport/Layout/PdfLayout.cs | 49 ++++++++++--------- .../TextMapping/PdfHeaderFooterCollection.cs | 15 ------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 2eec3baa5..1377da3cc 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -248,81 +248,83 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.ScaleHeaderFooterWithDocument = page.HeaderFooters.ScaleWithDocument; var hfType = page.HeaderFooters.GetPageType(physicalPageIndex); var leftH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Left); + bool alignHf = page.HeaderFooters.AlignWithMargins; + var (hfBoxLeft, hfBoxWidth) = HeaderFooterBox(pageSettings, alignHf); if (leftH != null && !leftH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, leftH, displayedPageNumber, totalPages)) { var ascent = leftH.Content.TextLines[0].LargestAscent; - var hfx = pageSettings.Margins.LeftPu; + var hfx = hfBoxLeft; var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; var text = new PdfCellContentLayout(pageSettings, dictionaries, leftH, hfx, hfy, 0, 0); text.ScaleAnchorX = hfx; // left edge text.ScaleAnchorY = hfy + ascent; text.Name = "LeftHeader"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Left, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Left, pageSettings, alignHf); } var centerH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Center); if (centerH != null && !centerH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, centerH, displayedPageNumber, totalPages)) { var ascent = centerH.Content.TextLines[0].LargestAscent; - var hfx = pageSettings.Margins.LeftPu; + var hfx = hfBoxLeft; var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; - var hfWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; + var hfWidth = hfBoxWidth; var text = new PdfCellContentLayout(pageSettings, dictionaries, centerH, hfx, hfy, hfWidth, 0); text.ScaleAnchorX = hfx + hfWidth / 2d; text.ScaleAnchorY = hfy + ascent; text.Name = "CenterHeader"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Center, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Center, pageSettings, alignHf); } var rightH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Right); if (rightH != null && !rightH.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, rightH, displayedPageNumber, totalPages)) { var ascent = rightH.Content.TextLines[0].LargestAscent; - var hfx = pageSettings.PageSize.WidthPu - pageSettings.Margins.RightPu; + var hfx = hfBoxLeft + hfBoxWidth; var hfy = pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent; var text = new PdfCellContentLayout(pageSettings, dictionaries, rightH, hfx, hfy, 0, 0); text.ScaleAnchorX = hfx; text.ScaleAnchorY = hfy + ascent; text.Name = "RightHeader"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Right, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Header, HeaderFooterAlignment.Right, pageSettings, alignHf); } var leftF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Left); if (leftF != null && !leftF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, leftF, displayedPageNumber, totalPages)) { int last = leftF.Content.TextLines.Count - 1; var descent = leftF.Content.TextLines[last].LargestDescent; - var hfx = pageSettings.Margins.LeftPu; + var hfx = hfBoxLeft; var hfy = pageSettings.Margins.FooterPu + descent; var text = new PdfCellContentLayout(pageSettings, dictionaries, leftF, hfx, hfy, 0, 0); text.ScaleAnchorX = hfx; text.ScaleAnchorY = hfy - descent; text.Name = "LeftFooter"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Left, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Left, pageSettings, alignHf); } var centerF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Center); if (centerF != null && !centerF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, centerF, displayedPageNumber, totalPages)) { int last = centerF.Content.TextLines.Count - 1; var descent = centerF.Content.TextLines[last].LargestDescent; - var hfx = pageSettings.PageSize.WidthPu / 2d; + var hfx = hfBoxLeft; var hfy = pageSettings.Margins.FooterPu + descent; - var hfWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; - var text = new PdfCellContentLayout(pageSettings, dictionaries, centerF, hfx, hfy, 0, 0); + var hfWidth = hfBoxWidth; + var text = new PdfCellContentLayout(pageSettings, dictionaries, centerF, hfx, hfy, hfWidth, 0); text.ScaleAnchorX = hfx + hfWidth / 2d; text.ScaleAnchorY = hfy - descent; text.Name = "CenterFooter"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Center, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Center, pageSettings, alignHf); } var rightF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Right); if (rightF != null && !rightF.HasImage && ShapeHeaderFooterText(pageSettings, dictionaries, rightF, displayedPageNumber, totalPages)) { int last = rightF.Content.TextLines.Count - 1; var descent = rightF.Content.TextLines[last].LargestDescent; - var hfx = pageSettings.PageSize.WidthPu - pageSettings.Margins.RightPu; + var hfx = hfBoxLeft + hfBoxWidth; var hfy = pageSettings.Margins.FooterPu + descent; var text = new PdfCellContentLayout(pageSettings, dictionaries, rightF, hfx, hfy, 0, 0); text.ScaleAnchorX = hfx; text.ScaleAnchorY = hfy - descent; text.Name = "RightFooter"; - AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Right, pageSettings); + AddHeaderFooterTextPart(pageLayout, dictionaries, text, HeaderFooterSection.Footer, HeaderFooterAlignment.Right, pageSettings, alignHf); } double hfContentLeft = pageSettings.Margins.LeftPu; double hfContentWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; @@ -375,7 +377,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio ? pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent : pageSettings.Margins.FooterPu + descent; double textY = System.Math.Min(naturalBaseline, imgBottom + descent); - var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(hfSection, hfAlign, pageSettings) }; + var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(hfSection, hfAlign, pageSettings, alignHf) }; double cursor = runStart; if (beforeRun != null) { @@ -1751,10 +1753,11 @@ private static bool ShapeHeaderFooterText(PdfPageSettings pageSettings, PdfDicti return hf.Content != null && hf.Content.TextLines != null && hf.Content.TextLines.Count > 0; } - private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps) + private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps, bool alignWithMargins) { - double left = ps.Margins.LeftPu; - double width = ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu; + //double left = ps.Margins.LeftPu; + //double width = ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu; + var (left, width) = HeaderFooterBox(ps, alignWithMargins); double x = align == HeaderFooterAlignment.Left ? left : align == HeaderFooterAlignment.Right ? left + width : left + width / 2d; @@ -1763,13 +1766,15 @@ private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFoot : ps.Margins.FooterPu; return new Vector2(x, y); } + private static (double Left, double Width) HeaderFooterBox(PdfPageSettings ps, bool alignWithMargins)=> alignWithMargins + ? (ps.Margins.LeftPu, ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu) + : (0d, ps.PageSize.WidthPu); - private static void AddHeaderFooterTextPart(PdfPageLayout pageLayout, PdfDictionaries dictionaries, - PdfCellContentLayout text, HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps) + private static void AddHeaderFooterTextPart(PdfPageLayout pageLayout, PdfDictionaries dictionaries, PdfCellContentLayout text, HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps, bool alignWithMargins) { text.IsHeaderFooter = true; text.GidsAndCharMap(dictionaries); - var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(section, align, ps) }; + var part = new PdfHeaderFooterPart { ScalePivot = HeaderFooterPivot(section, align, ps, alignWithMargins) }; part.Items.Add(text); pageLayout.HeaderFooterParts.Add(part); } diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs index 130d29f9d..c7204d51f 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs @@ -157,21 +157,6 @@ public PdfHeaderFooterCollection(PdfPageSettings pageSettings, PdfDictionaries d entry.Content.ContentAligmnet = PdfTextMap.GetAlignmentData(entry); PdfHeaderFooterEntries.Add(entry); } - - double hfScale = pageSettings.ContentScale; - if (ScaleWithDocument && hfScale != 1d) - { - foreach (var e in PdfHeaderFooterEntries) - { - if (e.Content?.TextFragments != null) - { - foreach (var tf in e.Content.TextFragments) - tf.Font.Size = (float)(tf.Font.Size * hfScale); - } - e.ImageWidth *= hfScale; - e.ImageHeight *= hfScale; - } - } } public PdfHeaderFooter Get(HeaderFooterType type, HeaderFooterSection section, HeaderFooterAlignment alignment) From 34386a394d69f7f567c511303cdf3f0084c69e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Mon, 12 Oct 2026 16:58:08 +0200 Subject: [PATCH 14/17] Align with page margins done --- src/EPPlus/ExcelHeaderFooter.cs | 2 +- src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/EPPlus/ExcelHeaderFooter.cs b/src/EPPlus/ExcelHeaderFooter.cs index 894ecd352..0288b137d 100644 --- a/src/EPPlus/ExcelHeaderFooter.cs +++ b/src/EPPlus/ExcelHeaderFooter.cs @@ -381,7 +381,7 @@ public bool AlignWithMargins { get { - return GetXmlNodeBool(alignWithMarginsPath); + return GetXmlNodeBool(alignWithMarginsPath, true); } set { diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 1377da3cc..e895c960d 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -1766,10 +1766,14 @@ private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFoot : ps.Margins.FooterPu; return new Vector2(x, y); } - private static (double Left, double Width) HeaderFooterBox(PdfPageSettings ps, bool alignWithMargins)=> alignWithMargins - ? (ps.Margins.LeftPu, ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu) - : (0d, ps.PageSize.WidthPu); + private static (double Left, double Width) HeaderFooterBox(PdfPageSettings ps, bool alignWithMargins) + { + if (alignWithMargins) + return (ps.Margins.LeftPu, ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu); + double d = UnitConversion.MmToPoints(19.05); + return (d, ps.PageSize.WidthPu - 2d * d); + } private static void AddHeaderFooterTextPart(PdfPageLayout pageLayout, PdfDictionaries dictionaries, PdfCellContentLayout text, HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps, bool alignWithMargins) { text.IsHeaderFooter = true; From fa9193175c913ec66fd329a37c728c256ad3bdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 13 Oct 2026 09:20:57 +0200 Subject: [PATCH 15/17] removed commented code. --- src/EPPlus.Export.Pdf/ExcelPdf.cs | 23 ------------------- .../Export/PdfExport/Layout/PdfLayout.cs | 2 -- 2 files changed, 25 deletions(-) diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 9e7064707..ebdfd3e8f 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -246,28 +246,6 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddCommand("Q"); } //Add header and footer. - //foreach (var hf in headerFooterLayouts) - //{ - // if (pageLayout.ScaleHeaderFooterWithDocument) - // { - // contentStream.AddCommand("q"); - // contentStream.AddContentScale(contentScale, hf.ScaleAnchorX, hf.ScaleAnchorY); - // } - // contentStream.AddCellContentLayout(hf, _dictionaries, pageSettings); - // if (pageLayout.ScaleHeaderFooterWithDocument) - // { - // contentStream.AddCommand("Q"); - // } - //} - ////Add images - //foreach (PdfImageLayout image in pageLayout.ChildObjects.OfType()) - //{ - // if (!image.IsHeaderFooter) continue; - // var imageResource = _dictionaries.AddImage(image.ImageBytes); - // contentStream.AddImage(imageResource.Label, image.LocalPosition.X, image.LocalPosition.Y, image.Size.X, image.Size.Y); - // if (PdfImageXObject.ProducesSoftMask(image.ImageBytes)) page.HasTransparency = true; - //} - //Add header and footer — each part scaled about its own pivot foreach (var part in pageLayout.HeaderFooterParts) { bool scaleHf = pageLayout.ScaleHeaderFooterWithDocument; @@ -295,7 +273,6 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) if (scaleHf) contentStream.AddCommand("Q"); } - //Add print titles contentStream.AddCommand("q"); contentStream.AddContentScale(contentScale, scaleAnchorX, scaleAnchorY); diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index e895c960d..3b899b6b5 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -1755,8 +1755,6 @@ private static bool ShapeHeaderFooterText(PdfPageSettings pageSettings, PdfDicti private static Vector2 HeaderFooterPivot(HeaderFooterSection section, HeaderFooterAlignment align, PdfPageSettings ps, bool alignWithMargins) { - //double left = ps.Margins.LeftPu; - //double width = ps.PageSize.WidthPu - ps.Margins.LeftPu - ps.Margins.RightPu; var (left, width) = HeaderFooterBox(ps, alignWithMargins); double x = align == HeaderFooterAlignment.Left ? left : align == HeaderFooterAlignment.Right ? left + width From 0244d3c6e3a632159d8e7943d4e4f411e2f921ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 13 Oct 2026 15:51:34 +0200 Subject: [PATCH 16/17] Added shrink to fit & better check clipping for spilling text. --- .../Layout/PdfCellContentLayout.cs | 8 ++- .../Export/PdfExport/Layout/PdfLayout.cs | 59 ++++++++++++++++++- .../PdfExport/TextShaping/PdfTextShaper.cs | 13 ++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs index 908fe7368..10c716033 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfCellContentLayout.cs @@ -205,14 +205,16 @@ private Vector2 CalculateRotatedAlignment(double textLength, double textHeight, return new Vector2(bx - minX, by - minY); } - internal void SetupClipping(double cellX, double cellY, double cellWidth, double cellHeight) + internal void SetupClipping(double cellX, double cellY, double cellWidth, double cellHeight, bool spillLeft = false, bool spillRight = false) { + double li = spillLeft ? 0d : rightMargin; + double ri = spillRight ? 0d : rightMargin; Clip = true; Clipping = new Rect() { - X = cellX + rightMargin, + X = cellX + li, Y = cellY - cellHeight, - Width = cellWidth - rightMargin * 2, + Width = cellWidth - li - ri, Height = cellHeight }; } diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 3b899b6b5..3258f1687 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -26,6 +26,7 @@ Date Author Change using OfficeOpenXml.Export.PdfExport.TextShaping; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Style; using OfficeOpenXml.Style.Dxf; @@ -204,8 +205,8 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var text = new PdfCellContentLayout(pageSettings, dictionaries, map, x, y, effectiveWidth, rowHeight); text.Name = map.Name; text.GidsAndCharMap(dictionaries); - if (NeedsClipping(map, pages[j], row, col)) - text.SetupClipping(x, y, effectiveWidth, rowHeight); + var (clipX, clipW, spillLeft, spillRight) = GetOverflowClip(map, pages[j], row, col, x, effectiveWidth); + text.SetupClipping(clipX, y, clipW, rowHeight, spillLeft, spillRight); pageLayout.AddChild(text); } } @@ -621,6 +622,60 @@ private static bool NeedsClipping(PdfCell map, Page page, int row, int col) } return false; } + private static (double clipX, double clipW, bool spillLeft, bool spillRight) GetOverflowClip(PdfCell map, Page page, int row, int col, double x, double effectiveWidth) + { + double clipX = x, clipW = effectiveWidth; + var a = map.ContentAligmnet; + if (a == null) + return (clipX, clipW, false, false); + var L = (EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment)ExcelHorizontalAlignment.Left; + var G = (EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment)ExcelHorizontalAlignment.General; + var R = (EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment)ExcelHorizontalAlignment.Right; + var C = (EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment)ExcelHorizontalAlignment.Center; + var F = (EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment)ExcelHorizontalAlignment.Fill; + var h = a.HorizontalAlignment; + if (h == F || a.WrapText || map.TotalTextLength <= map.ColumnWidth) + return (clipX, clipW, false, false); + double ScanRight() + { + double w = 0d; + for (int c = col + 1; c <= page.ToColumn; c++) + { + var n = page.Map[row, c]; + if (!string.IsNullOrEmpty(n.Text)) break; + w += n.ColumnWidth; + } + return w; + } + double ScanLeft() + { + double w = 0d; + for (int c = col - 1; c >= page.FromColumn; c--) + { + var n = page.Map[row, c]; + if (!string.IsNullOrEmpty(n.Text)) break; + w += n.ColumnWidth; + } + return w; + } + if (h == L || h == G) + { + clipW = effectiveWidth + ScanRight(); + return (clipX, clipW, false, true); + } + if (h == R) + { + double extra = ScanLeft(); + return (x - extra, effectiveWidth + extra, true, false); + } + if (h == C) + { + double left = ScanLeft(), right = ScanRight(); + return (x - left, effectiveWidth + left + right, true, true); + } + return (clipX, clipW, false, false); + } + private static bool HasBorder(PdfCellStyle cellStyle) { diff --git a/src/EPPlus/Export/PdfExport/TextShaping/PdfTextShaper.cs b/src/EPPlus/Export/PdfExport/TextShaping/PdfTextShaper.cs index 4344c29a0..d86753b15 100644 --- a/src/EPPlus/Export/PdfExport/TextShaping/PdfTextShaper.cs +++ b/src/EPPlus/Export/PdfExport/TextShaping/PdfTextShaper.cs @@ -83,6 +83,19 @@ public static void ShapeText(PdfPageSettings pageSettings, PdfDictionaries dicti cell.TextFragments[i] = tf; cell.ShapedTexts[i] = st; } + if (cell.ContentAligmnet?.ShrinkToFit == true && cell.ContentAligmnet?.WrapText != true && cell.ContentAligmnet?.IsVertical != true) + { + double hInset = 1.4d; + double cw = (cell.Merged && cell.Main == null) ? cell.Width : cell.ColumnWidth; + double avail = cw - 2d * hInset; + if (avail > 0 && totalTextLength > avail) + { + float shrink = (float)(avail / totalTextLength); + foreach (var tf in cell.TextFragments) + tf.Font.Size *= shrink; + totalTextLength *= shrink; + } + } if (cell.TextLayoutEngine != null) { double wrapWidth = (cell.Merged && cell.Main == null) ? cell.Width : cell.ColumnWidth; From b00cdbbd6fb6447a5950e3d94589c53891a4da3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 13 Oct 2026 16:32:45 +0200 Subject: [PATCH 17/17] Fixed fill selection for total row in tables. --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 13 +++++- .../PdfExport/TextMapping/PdfTextMap.cs | 45 ++++++++----------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index ac4da4f1c..756dea4b4 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -1307,12 +1307,23 @@ public void MultiSheet_PerSheetScaling_IsIsolated() $"combined ({combined}) should equal plain ({plainAlone}) + scaled ({scaledAlone}) - " + "per-sheet scaling must stay isolated"); } - public void TableDxfStyleTest() + + [TestMethod] + public void TableDxfStyleTest1() { using var p = OpenTemplatePackage("TableDxfStylePdf1.xlsx"); var ws = p.Workbook.Worksheets[0]; string path = _pdfPath + "TableDxfStylePdf1.pdf"; ws.SaveAsPdf(path); } + + [TestMethod] + public void TableDxfStyleTest2() + { + using var p = OpenTemplatePackage("TableDxfStylePdf2.xlsx"); + var ws = p.Workbook.Worksheets[0]; + string path = _pdfPath + "TableDxfStylePdf2.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs index fedb501b0..9fa2b13a1 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs @@ -190,40 +190,31 @@ private static void HandleMergedCell(PdfPageSettings pageSettings, PdfDictionari tempMap.Merged = true; } - private static IEnumerable GetApplicableTableElements( - ExcelTable table, ExcelTableNamedStyle ts, int tableRow, int tableCol, ExcelRangeBase cell) + private static IEnumerable GetApplicableTableElements(ExcelTable table, ExcelTableNamedStyle ts, int tableRow, int tableCol, ExcelRangeBase cell) { var range = table.Range; bool firstCol = table.ShowFirstColumn && tableCol == 0; bool lastCol = table.ShowLastColumn && cell._fromCol == range._toCol; bool header = table.ShowHeader && tableRow == 0; bool total = table.ShowTotal && cell._fromRow == range._toRow; + yield return ts.WholeTable; // 1 + // column-spanning: header + data + total + if (table.ShowColumnStripes) + yield return (tableCol & 1) == 0 ? ts.FirstColumnStripe // 2 + : ts.SecondColumnStripe; // 3 + // row stripes: data rows only + if (!header && !total && table.ShowRowStripes) + yield return (tableRow & 1) != 0 ? ts.FirstRowStripe // 4 + : ts.SecondRowStripe; // 5 - yield return ts.WholeTable; // 1 (base) - - if (header) - { - yield return ts.HeaderRow; // 9 - if (firstCol) yield return ts.FirstHeaderCell; // 10 - if (lastCol) yield return ts.LastHeaderCell; // 11 - } - else if (total) - { - yield return ts.TotalRow; // 8 - if (firstCol) yield return ts.FirstTotalCell; // 12 - if (lastCol) yield return ts.LastTotalCell; // 13 - } - else // data rows (no stripes on header/total) - { - if (table.ShowColumnStripes) - yield return (tableCol & 1) == 0 ? ts.FirstColumnStripe // 2 - : ts.SecondColumnStripe; // 3 - if (table.ShowRowStripes) - yield return (tableRow & 1) != 0 ? ts.FirstRowStripe // 4 - : ts.SecondRowStripe; // 5 - if (lastCol) yield return ts.LastColumn; // 6 - if (firstCol) yield return ts.FirstColumn; // 7 - } + if (lastCol) yield return ts.LastColumn; // 6 + if (firstCol) yield return ts.FirstColumn; // 7 + if (total) yield return ts.TotalRow; // 8 + if (header) yield return ts.HeaderRow; // 9 + if (header && firstCol) yield return ts.FirstHeaderCell; // 10 + if (header && lastCol) yield return ts.LastHeaderCell; // 11 + if (total && firstCol) yield return ts.FirstTotalCell; // 12 + if (total && lastCol) yield return ts.LastTotalCell; // 13 } // Overlay only the font properties `src` actually specifies onto `dst`. All members are on the base type.