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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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. From 7b418476f189c11ab8aa99617b858c731b646fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 14 Oct 2026 10:36:24 +0200 Subject: [PATCH 18/22] Added draft feature to pdf. --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 11 ++ .../Export/PdfExport/Layout/PdfLayout.cs | 151 ++++++++++-------- .../PdfExport/TextMapping/PdfTextMap.cs | 12 +- 3 files changed, 103 insertions(+), 71 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 756dea4b4..6dbeda3aa 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -23,6 +23,7 @@ Date Author Change using OfficeOpenXml.Style; using OfficeOpenXml.Table; using System.Data; +using System.Drawing.Printing; using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -1325,5 +1326,15 @@ public void TableDxfStyleTest2() string path = _pdfPath + "TableDxfStylePdf2.pdf"; ws.SaveAsPdf(path); } + + [TestMethod] + public void PdfIsDraftTest1() + { + using var p = OpenTemplatePackage("PDFTest.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.Draft = true; + string path = _pdfPath + "DraftTest1.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 3258f1687..6c742bc7b 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -89,6 +89,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio for (int i = 0; i < pdfPages.Count; i++) { var pageSettings = pdfPages[i].Settings; + bool isDraft = pageSettings.Draft; if (pdfPages[i].SheetIndex != currentSheetIndex) { currentSheetIndex = pdfPages[i].SheetIndex; @@ -140,17 +141,20 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pages[j].MergedCells.TryGetValue(key, out info)) { //Fill - var cellStyle = map.Main?.CellStyle ?? map.CellStyle; - var fill = new PdfCellLayout(info.X, info.Y, info.Width, info.Height); - SetFill(dictionaries, cellStyle, map.Text, fill); - fill.Name = map.Name; - fill.UpdateShadingPositionMatrix(pageSettings); - pageLayout.AddChild(fill); - //In-cell picture - var picMap = (map.CellPictureBytes != null) ? map : (map.Main != null && map.Main.CellPictureBytes != null) ? map.Main : null; - if (picMap != null) + if (!isDraft) { - AddCellPicture(pageLayout, picMap, info.X, info.Y, info.Width, info.Height); + var cellStyle = map.Main?.CellStyle ?? map.CellStyle; + var fill = new PdfCellLayout(info.X, info.Y, info.Width, info.Height); + SetFill(dictionaries, cellStyle, map.Text, fill); + fill.Name = map.Name; + fill.UpdateShadingPositionMatrix(pageSettings); + pageLayout.AddChild(fill); + //In-cell picture + var picMap = (map.CellPictureBytes != null) ? map : (map.Main != null && map.Main.CellPictureBytes != null) ? map.Main : null; + if (picMap != null) + { + AddCellPicture(pageLayout, picMap, info.X, info.Y, info.Width, info.Height); + } } //Text 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; @@ -162,26 +166,29 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio text.SetupClipping(info.X, info.Y, info.Width, info.Height); pageLayout.AddChild(text); } - if (map.Main != null) + if (!isDraft) { - var mergeMainStyle = map.Main.CellStyle; - if (HasDiagonalBorder(mergeMainStyle)) + if (map.Main != null) { - var diagBorder = new PdfCellBorderLayout( - isMerged: false, - corners: MergedCellCorners.All, - info: info, - x: info.X, - y: info.Y, - width: info.Width, - height: info.Height); - SetBorderStyle(mergeMainStyle, diagBorder); - diagBorder.Name = map.Name; - diagBorder.BorderData.Top.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - diagBorder.BorderData.Bottom.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - diagBorder.BorderData.Left.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - diagBorder.BorderData.Right.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - pageLayout.AddChild(diagBorder); + var mergeMainStyle = map.Main.CellStyle; + if (HasDiagonalBorder(mergeMainStyle)) + { + var diagBorder = new PdfCellBorderLayout( + isMerged: false, + corners: MergedCellCorners.All, + info: info, + x: info.X, + y: info.Y, + width: info.Width, + height: info.Height); + SetBorderStyle(mergeMainStyle, diagBorder); + diagBorder.Name = map.Name; + diagBorder.BorderData.Top.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + diagBorder.BorderData.Bottom.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + diagBorder.BorderData.Left.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + diagBorder.BorderData.Right.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + pageLayout.AddChild(diagBorder); + } } } drawnMergedCells.Add(key); @@ -192,13 +199,16 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio var contentRight = pageSettings.ContentBounds.Left + pageSettings.ContentBounds.Width; var effectiveWidth = GetClampedCellWidth(pageSettings, x, map.ColumnWidth); //Fill - var fill = new PdfCellLayout(x, y, effectiveWidth, rowHeight); - SetFill(dictionaries, map.CellStyle, map.Text, fill); - fill.UpdateShadingPositionMatrix(pageSettings); - fill.Name = map.Name; - pageLayout.AddChild(fill); - //In-cell picture - AddCellPicture(pageLayout, map, x, y, effectiveWidth, rowHeight); + if (!isDraft) + { + var fill = new PdfCellLayout(x, y, effectiveWidth, rowHeight); + SetFill(dictionaries, map.CellStyle, map.Text, fill); + fill.UpdateShadingPositionMatrix(pageSettings); + fill.Name = map.Name; + pageLayout.AddChild(fill); + //In-cell picture + AddCellPicture(pageLayout, map, x, y, effectiveWidth, rowHeight); + } //Text if (map.TextLines != null && map.TextLines.Count > 0) { @@ -210,38 +220,44 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } } - //Border - var borderStyle = (map.Merged && map.Main != null) ? map.Main.CellStyle : map.CellStyle; - if (HasBorder(map.CellStyle)) + if (!isDraft) { - var border = new PdfCellBorderLayout(map.Merged, GetCorners(map.MergedAddress, row, col), info, x, y, map.ColumnWidth, rowHeight); - SetBorderStyle(map.CellStyle, border); - border.Name = map.Name; - if (map.Merged && map.MergedAddress != null) + //Border + var borderStyle = (map.Merged && map.Main != null) ? map.Main.CellStyle : map.CellStyle; + if (HasBorder(map.CellStyle)) { - var addr = map.MergedAddress; - if (row != addr.Start.Row) border.BorderData.Top.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - if (row != addr.End.Row) border.BorderData.Bottom.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - if (col != addr.Start.Column) border.BorderData.Left.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; - if (col != addr.End.Column) border.BorderData.Right.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + var border = new PdfCellBorderLayout(map.Merged, GetCorners(map.MergedAddress, row, col), info, x, y, map.ColumnWidth, rowHeight); + SetBorderStyle(map.CellStyle, border); + border.Name = map.Name; + if (map.Merged && map.MergedAddress != null) + { + var addr = map.MergedAddress; + if (row != addr.Start.Row) border.BorderData.Top.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + if (row != addr.End.Row) border.BorderData.Bottom.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + if (col != addr.Start.Column) border.BorderData.Left.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + if (col != addr.End.Column) border.BorderData.Right.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; + } + SetDoubleBorderMiterFlags(pages[j], row, col, border); + pageLayout.AddChild(border); } - SetDoubleBorderMiterFlags(pages[j], row, col, border); - pageLayout.AddChild(border); } x += map.ColumnWidth; } y -= rowHeight; x = contentStartX; } - if (page.Images != null) + if (!isDraft) { - foreach (var img in page.Images) + if (page.Images != null) { - pageLayout.AddChild(new PdfImageLayout(img.X, img.Y, img.Width, img.Height) + foreach (var img in page.Images) { - ImageBytes = img.ImageBytes, - Name = img.Name, - }); + pageLayout.AddChild(new PdfImageLayout(img.X, img.Y, img.Width, img.Height) + { + ImageBytes = img.ImageBytes, + Name = img.Name, + }); + } } } if (page.HeaderFooters != null) @@ -424,7 +440,8 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio cellcont.GidsAndCharMap(dictionaries); pageLayout.AddChild(cellcont); } - PdfGridlinesLayout.AddGridLines(pageSettings, pages[j], pageLayout, borderOnly: !pageSettings.ShowGridLines || pdfPages[i].IsCommentsPage); + if(!isDraft) + PdfGridlinesLayout.AddGridLines(pageSettings, pages[j], pageLayout, borderOnly: !pageSettings.ShowGridLines || pdfPages[i].IsCommentsPage); pageLayout.ChildObjects.Sort((a, b) => { int cmp = a.Z.CompareTo(b.Z); @@ -710,7 +727,8 @@ private static void AddHeadingCell(PdfPageSettings pageSettings, PdfDictionaries text.GidsAndCharMap(dictionaries); pageLayout.AddChild(text); } - AddHeadingCellBorder(pageLayout, x, y, width, height); + if(!pageSettings.Draft) + AddHeadingCellBorder(pageLayout, x, y, width, height); } private static void AddHeadingCells(PdfPageSettings pageSettings, PdfDictionaries dictionaries, Page page, PdfPageLayout pageLayout, double contentStartX, double contentStartY, double headingWidth, double headingHeight, string fontName, float fontSize, ExcelFill fill) @@ -732,15 +750,18 @@ private static void AddHeadingCells(PdfPageSettings pageSettings, PdfDictionarie x, GetOriginY(pageSettings, page), colWidth, headingHeight, fontName, fontSize, "Heading_Col_" + colLetter); x += colWidth; } - double y = contentStartY; - for (int row = page.FromRow; row <= page.ToRow; row++) + if (!pageSettings.Draft) { - double rowHeight = page.RowHeights[row - page.FromRow]; - if (rowHeight == 0d) { y -= rowHeight; continue; } - string rowNum = row.ToString(); - AddHeadingCell(pageSettings, dictionaries, pageLayout, headingStyle, rowNum, - GetOriginX(pageSettings, page), y, headingWidth, rowHeight, fontName, fontSize, "Heading_Row_" + rowNum); - y -= rowHeight; + double y = contentStartY; + for (int row = page.FromRow; row <= page.ToRow; row++) + { + double rowHeight = page.RowHeights[row - page.FromRow]; + if (rowHeight == 0d) { y -= rowHeight; continue; } + string rowNum = row.ToString(); + AddHeadingCell(pageSettings, dictionaries, pageLayout, headingStyle, rowNum, + GetOriginX(pageSettings, page), y, headingWidth, rowHeight, fontName, fontSize, "Heading_Row_" + rowNum); + y -= rowHeight; + } } } diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs index 9fa2b13a1..b127bcd53 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs @@ -207,14 +207,14 @@ private static IEnumerable GetApplicableTableElements(Ex yield return (tableRow & 1) != 0 ? ts.FirstRowStripe // 4 : ts.SecondRowStripe; // 5 - if (lastCol) yield return ts.LastColumn; // 6 + 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 (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 + 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. From 30eeb35b26fd629c02fc60ff89a51713fb14aff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Wed, 14 Oct 2026 14:23:00 +0200 Subject: [PATCH 19/22] Added feature for black and white PDF export. --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 10 + .../DocumentObjects/PdfContentStream.cs | 2 +- .../DocumentObjects/PdfImageXObject.cs | 28 +- src/EPPlus.Export.Pdf/ExcelPdf.cs | 2 +- .../Helpers/JpegBlackWhiteHelper.cs | 289 ++++++++++++++++++ src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs | 43 ++- .../Resources/PdfImageResource .cs | 4 +- .../Settings/PdfDocumentSettings.cs | 2 + .../Export/PdfExport/Layout/PdfLayout.cs | 49 +-- 9 files changed, 403 insertions(+), 26 deletions(-) create mode 100644 src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 6dbeda3aa..c76546804 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -1336,5 +1336,15 @@ public void PdfIsDraftTest1() string path = _pdfPath + "DraftTest1.pdf"; ws.SaveAsPdf(path); } + + [TestMethod] + public void PdfIsBlackAndWhiteTest1() + { + using var p = OpenTemplatePackage("PDFTest.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.BlackAndWhite = true; + string path = _pdfPath + "BlackAndWhiteTest1.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs index 1c8d972c9..1736b2d26 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs @@ -235,7 +235,7 @@ public void AddText(PdfCellContentLayout cell, Vector2 position, double textRota commands.Add($"S"); } commands.Add("BT"); - commands.Add(color.ToFillCommand()); + commands.Add((pageSettings.BlackAndWhite ? Color.Black : color).ToFillCommand()); commands.Add($"{textMatrix.A.ToPdfStringF4()} {textMatrix.B.ToPdfStringF4()} {textMatrix.C.ToPdfStringF4()} {textMatrix.D.ToPdfStringF4()} {textMatrix.E.ToPdfStringF4()} {textMatrix.F.ToPdfStringF4()} Tm"); byte currentFontId = shapedText.ShapedText.Glyphs.Length > 0 ? shapedText.ShapedText.Glyphs[0].FontId : (byte)0; string currentFontLabel = shapedText.FontIdMap.ContainsKey(currentFontId) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs index 71a728cd8..8a369d8ca 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs @@ -30,7 +30,7 @@ internal class PdfImageXObject : PdfObject internal byte[] SoftMaskData { get; } internal int SoftMaskObjectNumber { get; set; } - public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) + public PdfImageXObject(int objectNumber, byte[] imageBytes, bool blackAndWhite, int version = 0) : base(objectNumber, version) { byte[] icoDib = null; @@ -46,6 +46,7 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) BitsPerComponent = 8; ColorSpace = "/DeviceRGB"; Filter = "FlateDecode"; + if (blackAndWhite) GrayscaleRgb(icoRgb); _bytes = PdfFlate.CompressLeaveOpen(icoRgb); if (icoAlpha != null) { @@ -55,7 +56,7 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) } else if (JpegDecoder.IsJpeg(imageBytes)) { - _bytes = imageBytes; + _bytes = blackAndWhite ? JpegBlackWhiteHelper.MakeGrayscale(imageBytes) : imageBytes; Filter = "DCTDecode"; BitsPerComponent = 8; JpegDecoder.ReadJpegInfo(imageBytes, out int width, out int height, out int components, out bool adobe); @@ -81,11 +82,18 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) { BitsPerComponent = 8; ColorSpace = colorType == 6 ? "/DeviceRGB" : "/DeviceGray"; - PngDecoder.DecodePngWithAlpha(imageBytes, width, height, colorType, out byte[] color, out byte[] alpha); + PngDecoder.DecodePngWithAlpha(imageBytes, width, height, blackAndWhite, colorType, out byte[] color, out byte[] alpha); _bytes = color; SoftMaskData = alpha; HasSoftMask = true; } + else if (colorType == 2 && blackAndWhite) + { + BitsPerComponent = 8; + ColorSpace = "/DeviceRGB"; + PngDecoder.DecodePngTruecolor(imageBytes, width, height, blackAndWhite, out byte[] grayRgb); + _bytes = grayRgb; + } else { BitsPerComponent = bitDepth; @@ -98,6 +106,8 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) colors = 1; break; case 3: + if (blackAndWhite && palette != null) + GrayscaleRgb(palette); int hival = palette == null || palette.Length < 3 ? 0 : (palette.Length / 3) - 1; ColorSpace = "[ /Indexed /DeviceRGB " + hival + " <" + PngDecoder.ToHex(palette) + "> ]"; colors = 1; @@ -124,6 +134,7 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) BitsPerComponent = 8; ColorSpace = "/DeviceRGB"; Filter = "FlateDecode"; + if (blackAndWhite) GrayscaleRgb(bmpRgb); _bytes = PdfFlate.CompressLeaveOpen(bmpRgb); } else if (GifDecoder.TryDecode(imageBytes, out int gifW, out int gifH, out byte[] gifRgb, out byte[] gifAlpha)) @@ -132,6 +143,7 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) BitsPerComponent = 8; ColorSpace = "/DeviceRGB"; Filter = "FlateDecode"; + if (blackAndWhite) GrayscaleRgb(gifRgb); _bytes = PdfFlate.CompressLeaveOpen(gifRgb); if (gifAlpha != null) { SoftMaskData = PdfFlate.CompressLeaveOpen(gifAlpha); HasSoftMask = true; } } @@ -142,6 +154,7 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) BitsPerComponent = 8; ColorSpace = "/DeviceRGB"; Filter = "FlateDecode"; + if (blackAndWhite) GrayscaleRgb(tifRgb); _bytes = PdfFlate.CompressLeaveOpen(tifRgb); if (tifAlpha != null) { @@ -170,6 +183,15 @@ private PdfImageXObject(int objectNumber, int version, byte[] deflatedGray, int Filter = "FlateDecode"; } + internal static void GrayscaleRgb(byte[] rgb) + { + for (int i = 0; i + 2 < rgb.Length; i += 3) + { + byte y = (byte)(0.2126 * rgb[i] + 0.7152 * rgb[i + 1] + 0.0722 * rgb[i + 2] + 0.5); + rgb[i] = rgb[i + 1] = rgb[i + 2] = y; + } + } + internal static PdfImageXObject CreateSoftMask(int objectNumber, byte[] deflatedGray, int width, int height) => new PdfImageXObject(objectNumber, 0, deflatedGray, width, height); internal static bool CanEmbed(byte[] imageBytes) diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index ebdfd3e8f..bfb0549ff 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -137,7 +137,7 @@ private void AddImageData() { foreach (var image in _dictionaries.Images) { - var img = image.Value.GetImageObject(_document.Count + 1); + var img = image.Value.GetImageObject(_document.Count + 1, _documentSettings.BlackAndWhite); if (img.HasSoftMask) { var mask = PdfImageXObject.CreateSoftMask(_document.Count + 1, img.SoftMaskData, img.Width, img.Height); diff --git a/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs new file mode 100644 index 000000000..93c1b054d --- /dev/null +++ b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs @@ -0,0 +1,289 @@ +/************************************************************************************************* + 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 System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.Pdf.Helpers +{ + internal static class JpegBlackWhiteHelper + { + internal static byte[] MakeGrayscale(byte[] jpeg) + { + try { return TryTransform(jpeg) ?? jpeg; } + catch { return jpeg; } + } + + private sealed class Huff + { + public byte[] Bits = new byte[17]; // Bits[1..16] = count of codes of that length + public byte[] Vals; // HUFFVAL + public int[] MinCode = new int[17]; + public int[] MaxCode = new int[17]; + public int[] ValPtr = new int[17]; + public int[] EncCode = new int[256]; + public int[] EncLen = new int[256]; + + public void Build() + { + int lastk = 0; + var size = new int[257]; + for (int l = 1; l <= 16; l++) + for (int i = 0; i < Bits[l]; i++) size[lastk++] = l; + size[lastk] = 0; + + var code = new int[257]; + int k = 0, c = 0, si = size[0]; + if (lastk > 0) + { + while (true) + { + do { code[k] = c; c++; k++; } while (size[k] == si); + if (size[k] == 0) break; + do { c <<= 1; si++; } while (size[k] != si); + } + } + + k = 0; + for (int l = 1; l <= 16; l++) + { + if (Bits[l] == 0) { MaxCode[l] = -1; } + else { ValPtr[l] = k; MinCode[l] = code[k]; k += Bits[l]; MaxCode[l] = code[k - 1]; } + } + for (int idx = 0; idx < lastk; idx++) + { + EncCode[Vals[idx]] = code[idx]; + EncLen[Vals[idx]] = size[idx]; + } + } + } + + private sealed class BitReader + { + private readonly byte[] d; private int pos; private readonly int end; + private int buf, cnt; + public BitReader(byte[] data, int start, int stop) { d = data; pos = start; end = stop; } + + public int Bit() + { + if (cnt == 0) + { + if (pos >= end) return 0; + int b = d[pos++] & 0xFF; + if (b == 0xFF && pos < end && (d[pos] & 0xFF) == 0x00) pos++; // unstuff + buf = b; cnt = 8; + } + cnt--; return (buf >> cnt) & 1; + } + public int Bits(int n) { int v = 0; while (n-- > 0) v = (v << 1) | Bit(); return v; } + + public void RestartAlign() + { + cnt = 0; // discard partial bits + if (pos + 1 < end && (d[pos] & 0xFF) == 0xFF + && (d[pos + 1] & 0xFF) >= 0xD0 && (d[pos + 1] & 0xFF) <= 0xD7) + pos += 2; // consume RSTn + } + } + + private sealed class BitWriter + { + public readonly List Out = new List(); + private int acc, cnt; + public void Write(int code, int len) + { + for (int i = len - 1; i >= 0; i--) + { + acc = (acc << 1) | ((code >> i) & 1); + if (++cnt == 8) + { + byte v = (byte)acc; Out.Add(v); + if (v == 0xFF) Out.Add(0x00); // stuff + acc = 0; cnt = 0; + } + } + } + public void FlushToByte() { if (cnt > 0) Write(0x7F, 8 - cnt); } // pad with 1-bits (libjpeg-style) + private int rst; + public void EmitRst() { FlushToByte(); Out.Add(0xFF); Out.Add((byte)(0xD0 + (rst & 7))); rst++; } + } + + private static byte[] TryTransform(byte[] d) + { + if (d == null || d.Length < 4 || (d[0] & 0xFF) != 0xFF || (d[1] & 0xFF) != 0xD8) return null; + + var dcT = new Huff[4]; var acT = new Huff[4]; + int comps = 0; + var cid = new int[4]; var cH = new int[4]; var cV = new int[4]; + int W = 0, H = 0, Hmax = 1, Vmax = 1, dri = 0; + int sosEnd = -1, scanTermMarker = -1; + // SOS component -> (sofIndex, Td, Ta) + int sNs = 0; var sSof = new int[4]; var sTd = new int[4]; var sTa = new int[4]; + + int i = 2; + while (i + 1 < d.Length) + { + if ((d[i] & 0xFF) != 0xFF) { i++; continue; } + int m = d[i + 1] & 0xFF; + if (m == 0xFF) { i++; continue; } + if (m == 0xD9) break; // EOI + if (m == 0xC2 || m == 0xC3 || (m >= 0xC5 && m <= 0xCB) || (m >= 0xCD && m <= 0xCF)) + return null; // progressive / arithmetic / unsupported + int seg = ((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF); + int p = i + 4; + + if (m == 0xC0 || m == 0xC1) // SOF0 / SOF1 (baseline / ext. sequential) + { + H = ((d[p + 1] & 0xFF) << 8) | (d[p + 2] & 0xFF); + W = ((d[p + 3] & 0xFF) << 8) | (d[p + 4] & 0xFF); + comps = d[p + 5] & 0xFF; + if (comps != 3) return null; // only YCbCr handled here + for (int cc = 0; cc < comps; cc++) + { + int q = p + 6 + cc * 3; + cid[cc] = d[q] & 0xFF; + cH[cc] = (d[q + 1] >> 4) & 0xF; + cV[cc] = d[q + 1] & 0xF; + if (cH[cc] > Hmax) Hmax = cH[cc]; + if (cV[cc] > Vmax) Vmax = cV[cc]; + } + } + else if (m == 0xC4) // DHT (may hold several tables) + { + int q = p, segEnd = i + 2 + seg; + while (q < segEnd) + { + int tc = (d[q] >> 4) & 0xF, th = d[q] & 0xF; q++; + var t = new Huff(); int tot = 0; + for (int l = 1; l <= 16; l++) { t.Bits[l] = d[q++]; tot += t.Bits[l]; } + t.Vals = new byte[tot]; + for (int v = 0; v < tot; v++) t.Vals[v] = d[q++]; + t.Build(); + if (tc == 0) dcT[th] = t; else acT[th] = t; + } + } + else if (m == 0xDD) { dri = ((d[p] & 0xFF) << 8) | (d[p + 1] & 0xFF); } // DRI + else if (m == 0xDA) // SOS + { + sNs = d[p] & 0xFF; + for (int s = 0; s < sNs; s++) + { + int selId = d[p + 1 + s * 2] & 0xFF; + int tdta = d[p + 2 + s * 2] & 0xFF; + int sof = 0; for (int cc = 0; cc < comps; cc++) if (cid[cc] == selId) sof = cc; + sSof[s] = sof; sTd[s] = (tdta >> 4) & 0xF; sTa[s] = tdta & 0xF; + } + sosEnd = p + 1 + sNs * 2 + 3; // after Ss,Se,Ah/Al + // find the marker that terminates the entropy stream + int j = sosEnd; + while (j + 1 < d.Length) + { + if ((d[j] & 0xFF) == 0xFF) + { + int mm = d[j + 1] & 0xFF; + if (mm != 0x00 && !(mm >= 0xD0 && mm <= 0xD7)) { scanTermMarker = j; break; } + } + j++; + } + break; + } + i += 2 + seg; + } + + if (sosEnd < 0 || scanTermMarker < 0 || W == 0 || H == 0) return null; + if (sNs != 3) return null; + + var br = new BitReader(d, sosEnd, scanTermMarker); + var bw = new BitWriter(); + + int mcuX = (W + 8 * Hmax - 1) / (8 * Hmax); + int mcuY = (H + 8 * Vmax - 1) / (8 * Vmax); + long totalMcu = (long)mcuX * mcuY; + + for (long mcu = 0; mcu < totalMcu; mcu++) + { + if (dri > 0 && mcu > 0 && mcu % dri == 0) { bw.EmitRst(); br.RestartAlign(); } + + for (int s = 0; s < sNs; s++) + { + var dc = dcT[sTd[s]]; var ac = acT[sTa[s]]; + if (dc == null || ac == null) return null; + bool luma = sSof[s] == 0; + int blocks = cH[sSof[s]] * cV[sSof[s]]; + + for (int b = 0; b < blocks; b++) + { + // ---- DC ---- + int t = Decode(br, dc); + int dcBits = br.Bits(t); + if (luma) + { + if (dc.EncLen[t] == 0) return null; + bw.Write(dc.EncCode[t], dc.EncLen[t]); + if (t > 0) bw.Write(dcBits, t); + } + // ---- AC ---- + int k = 1; + while (k < 64) + { + int rs = Decode(br, ac); + int run = rs >> 4, size = rs & 0xF; + if (size == 0) + { + if (luma) { if (ac.EncLen[rs] == 0) return null; bw.Write(ac.EncCode[rs], ac.EncLen[rs]); } + if (run != 15) break; // EOB + k += 16; continue; // ZRL + } + int acBits = br.Bits(size); + if (luma) + { + if (ac.EncLen[rs] == 0) return null; + bw.Write(ac.EncCode[rs], ac.EncLen[rs]); + bw.Write(acBits, size); + } + k += run + 1; + } + + if (!luma) // emit one neutral (zero) chroma block + { + if (dc.EncLen[0] == 0 || ac.EncLen[0] == 0) return null; + bw.Write(dc.EncCode[0], dc.EncLen[0]); // DC category 0 (diff 0) + bw.Write(ac.EncCode[0], ac.EncLen[0]); // AC EOB + } + } + } + } + + bw.FlushToByte(); + + var outp = new List(d.Length); + for (int k = 0; k < sosEnd; k++) outp.Add(d[k]); // headers + SOS header, verbatim + outp.AddRange(bw.Out); // re-encoded entropy stream + for (int k = scanTermMarker; k < d.Length; k++) outp.Add(d[k]); // EOI + trailer, verbatim + return outp.ToArray(); + } + + private static int Decode(BitReader br, Huff h) + { + int l = 1, code = br.Bit(); + while (h.MaxCode[l] < 0 || code > h.MaxCode[l]) + { + code = (code << 1) | br.Bit(); + if (++l > 16) return 0; + } + return h.Vals[h.ValPtr[l] + code - h.MinCode[l]] & 0xFF; + } + } +} diff --git a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs index c4efdc62c..5578bd6b1 100644 --- a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs +++ b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.DocumentObjects; using System.IO; using System.Text; @@ -73,7 +74,45 @@ internal static byte[] ReadPngIdat(byte[] d, out byte[] palette) } } - internal static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, int colorType, out byte[] deflatedColor, out byte[] deflatedAlpha) + internal static void DecodePngTruecolor(byte[] pngBytes, int width, int height, bool blackAndWhite, out byte[] deflatedRgb) + { + const int channels = 3; + byte[] filtered = PdfFlate.Decompress(ReadPngIdat(pngBytes, out byte[] _)); + int stride = width * channels; + var rgb = new byte[width * height * channels]; + var prev = new byte[stride]; + var cur = new byte[stride]; + int pos = 0, ci = 0; + for (int y = 0; y < height; y++) + { + int filter = pos < filtered.Length ? filtered[pos++] : 0; + for (int x = 0; x < stride; x++) + { + int raw = pos < filtered.Length ? filtered[pos++] : 0; + int a = x >= channels ? cur[x - channels] : 0; + int b = prev[x]; + int c = x >= channels ? prev[x - channels] : 0; + int val; + switch (filter) + { + case 1: val = raw + a; break; + case 2: val = raw + b; break; + case 3: val = raw + ((a + b) >> 1); break; + case 4: val = raw + Paeth(a, b, c); break; + default: val = raw; break; + } + cur[x] = (byte)(val & 0xFF); + } + System.Array.Copy(cur, 0, rgb, ci, stride); + ci += stride; + var swap = prev; prev = cur; cur = swap; + } + if (blackAndWhite) + PdfImageXObject.GrayscaleRgb(rgb); + deflatedRgb = PdfFlate.CompressLeaveOpen(rgb); + } + + internal static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, bool blackAndWhite, int colorType, out byte[] deflatedColor, out byte[] deflatedAlpha) { int channels = colorType == 6 ? 4 : 2; int colorChannels = colorType == 6 ? 3 : 1; @@ -133,6 +172,8 @@ internal static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, } var swap = prev; prev = cur; cur = swap; } + if (blackAndWhite && colorType == 6) // <-- raw RGB, BEFORE compression + PdfImageXObject.GrayscaleRgb(color); deflatedColor = PdfFlate.CompressLeaveOpen(color); deflatedAlpha = PdfFlate.CompressLeaveOpen(alpha); } diff --git a/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs b/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs index 08fe9e15d..7e7a96b8a 100644 --- a/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs +++ b/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs @@ -25,10 +25,10 @@ public PdfImageResource(int labelNumber, byte[] imageBytes) ImageBytes = imageBytes; } - public PdfImageXObject GetImageObject(int objectNumber, int version = 0) + public PdfImageXObject GetImageObject(int objectNumber, bool blackAndWhite, int version = 0) { this.objectNumber = objectNumber; - return new PdfImageXObject(objectNumber, ImageBytes, version); + return new PdfImageXObject(objectNumber, ImageBytes, blackAndWhite, version); } } } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs index 45485e53a..a0e9fede7 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs @@ -29,6 +29,7 @@ internal class PdfDocumentSettings internal string LicenseHolder; internal string Title; internal string Author; + internal bool BlackAndWhite; internal static PdfDocumentSettings From(PdfPageSettings s) { @@ -46,6 +47,7 @@ internal static PdfDocumentSettings From(PdfPageSettings s) LicenseHolder = s.LicenseHolder, Title = s.Title, Author = s.Author, + BlackAndWhite = s.BlackAndWhite }; } diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index 6c742bc7b..dacd5e548 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -90,6 +90,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio { var pageSettings = pdfPages[i].Settings; bool isDraft = pageSettings.Draft; + bool isBlackWhite = pageSettings.BlackAndWhite; if (pdfPages[i].SheetIndex != currentSheetIndex) { currentSheetIndex = pdfPages[i].SheetIndex; @@ -140,15 +141,18 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio if (!drawnMergedCells.Contains(key) && pages[j].MergedCells.TryGetValue(key, out info)) { - //Fill if (!isDraft) { - var cellStyle = map.Main?.CellStyle ?? map.CellStyle; - var fill = new PdfCellLayout(info.X, info.Y, info.Width, info.Height); - SetFill(dictionaries, cellStyle, map.Text, fill); - fill.Name = map.Name; - fill.UpdateShadingPositionMatrix(pageSettings); - pageLayout.AddChild(fill); + //Fill + if (!isBlackWhite) + { + var cellStyle = map.Main?.CellStyle ?? map.CellStyle; + var fill = new PdfCellLayout(info.X, info.Y, info.Width, info.Height); + SetFill(dictionaries, cellStyle, map.Text, fill); + fill.Name = map.Name; + fill.UpdateShadingPositionMatrix(pageSettings); + pageLayout.AddChild(fill); + } //In-cell picture var picMap = (map.CellPictureBytes != null) ? map : (map.Main != null && map.Main.CellPictureBytes != null) ? map.Main : null; if (picMap != null) @@ -181,7 +185,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio y: info.Y, width: info.Width, height: info.Height); - SetBorderStyle(mergeMainStyle, diagBorder); + SetBorderStyle(mergeMainStyle, diagBorder, isBlackWhite); diagBorder.Name = map.Name; diagBorder.BorderData.Top.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; diagBorder.BorderData.Bottom.BorderStyle = (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)ExcelBorderStyle.None; @@ -198,14 +202,17 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio { var contentRight = pageSettings.ContentBounds.Left + pageSettings.ContentBounds.Width; var effectiveWidth = GetClampedCellWidth(pageSettings, x, map.ColumnWidth); - //Fill if (!isDraft) { - var fill = new PdfCellLayout(x, y, effectiveWidth, rowHeight); - SetFill(dictionaries, map.CellStyle, map.Text, fill); - fill.UpdateShadingPositionMatrix(pageSettings); - fill.Name = map.Name; - pageLayout.AddChild(fill); + if (!isBlackWhite) + { + //Fill + var fill = new PdfCellLayout(x, y, effectiveWidth, rowHeight); + SetFill(dictionaries, map.CellStyle, map.Text, fill); + fill.UpdateShadingPositionMatrix(pageSettings); + fill.Name = map.Name; + pageLayout.AddChild(fill); + } //In-cell picture AddCellPicture(pageLayout, map, x, y, effectiveWidth, rowHeight); } @@ -227,7 +234,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio if (HasBorder(map.CellStyle)) { var border = new PdfCellBorderLayout(map.Merged, GetCorners(map.MergedAddress, row, col), info, x, y, map.ColumnWidth, rowHeight); - SetBorderStyle(map.CellStyle, border); + SetBorderStyle(map.CellStyle, border, isBlackWhite); border.Name = map.Name; if (map.Merged && map.MergedAddress != null) { @@ -865,7 +872,7 @@ private static void AddPrintTitleCells(PdfPageSettings pageSettings, PdfDictiona if (!map.Merged && HasBorder(map.CellStyle)) { var border = new PdfCellBorderLayout(false, MergedCellCorners.All, new MergedCellDrawInfo(), t.X, t.Y, t.Width, t.Height); - SetBorderStyle(map.CellStyle, border); + SetBorderStyle(map.CellStyle, border, pageSettings.BlackAndWhite); border.IsPrintTitle = true; border.Name = "PrintTitleBorder_" + map.Name; pageLayout.AddChild(border); @@ -875,7 +882,7 @@ private static void AddPrintTitleCells(PdfPageSettings pageSettings, PdfDictiona var sub = b.Cell; if (!HasBorder(sub.CellStyle)) continue; var border = new PdfCellBorderLayout(false, MergedCellCorners.All, new MergedCellDrawInfo(), b.X, b.Y, b.Width, b.Height); - SetBorderStyle(sub.CellStyle, border); + SetBorderStyle(sub.CellStyle, border, pageSettings.BlackAndWhite); border.IsPrintTitle = true; border.Name = "PrintTitleMergeBorder_" + sub.Name; pageLayout.AddChild(border); @@ -883,25 +890,31 @@ private static void AddPrintTitleCells(PdfPageSettings pageSettings, PdfDictiona } } - private static void SetBorderStyle(PdfCellStyle style, PdfCellBorderLayout border) + private static void SetBorderStyle(PdfCellStyle style, PdfCellBorderLayout border, bool isBlackAndWhite) { var topStyle = style.xfTop.Style == ExcelBorderStyle.None ? ((style.dxfTop != null && style.dxfTop.HasValue) ? (ExcelBorderStyle)style.dxfTop.Style : ExcelBorderStyle.None) : style.xfTop.Style; var topColor = (style.xfTop.Style == ExcelBorderStyle.None && style.dxfTop != null) ? PdfColor.SetColorFromHex(style.dxfTop.Color.LookupColor(style.dxfTop)) : PdfColor.SetColorFromHex(style.xfTop.Color.LookupColor(style.xfTop)); + topColor = isBlackAndWhite ? Color.Black : topColor; var bottomStyle = style.xfBottom.Style == ExcelBorderStyle.None ? ((style.dxfBottom != null && style.dxfBottom.HasValue) ? (ExcelBorderStyle)style.dxfBottom.Style : ExcelBorderStyle.None) : style.xfBottom.Style; var bottomColor = (style.xfBottom.Style == ExcelBorderStyle.None && style.dxfBottom != null) ? PdfColor.SetColorFromHex(style.dxfBottom.Color.LookupColor(style.dxfBottom)) : PdfColor.SetColorFromHex(style.xfBottom.Color.LookupColor(style.xfBottom)); + bottomColor = isBlackAndWhite ? Color.Black : bottomColor; var leftStyle = style.xfLeft.Style == ExcelBorderStyle.None ? ((style.dxfLeft != null && style.dxfLeft.HasValue) ? (ExcelBorderStyle)style.dxfLeft.Style : ExcelBorderStyle.None) : style.xfLeft.Style; var leftColor = (style.xfLeft.Style == ExcelBorderStyle.None && style.dxfLeft != null) ? PdfColor.SetColorFromHex(style.dxfLeft.Color.LookupColor(style.dxfLeft)) : PdfColor.SetColorFromHex(style.xfLeft.Color.LookupColor(style.xfLeft)); + leftColor = isBlackAndWhite ? Color.Black : leftColor; var rightStyle = style.xfRight.Style == ExcelBorderStyle.None ? ((style.dxfRight != null && style.dxfRight.HasValue) ? (ExcelBorderStyle)style.dxfRight.Style : ExcelBorderStyle.None) : style.xfRight.Style; var rightColor = (style.xfRight.Style == ExcelBorderStyle.None && style.dxfRight != null) ? PdfColor.SetColorFromHex(style.dxfRight.Color.LookupColor(style.dxfRight)) : PdfColor.SetColorFromHex(style.xfRight.Color.LookupColor(style.xfRight)); + rightColor = isBlackAndWhite ? Color.Black : rightColor; var diagUpStyle = style.DiagonalUp ? style.Diagonal.Style : ExcelBorderStyle.None; var diagUpColor = style.DiagonalUp ? PdfColor.SetColorFromHex(style.Diagonal.Color.LookupColor(style.Diagonal)) : Color.Transparent; + diagUpColor = isBlackAndWhite ? Color.Black : diagUpColor; var diagDownStyle = style.DiagonalDown ? style.Diagonal.Style : ExcelBorderStyle.None; var diagDownColor = style.DiagonalDown ? PdfColor.SetColorFromHex(style.Diagonal.Color.LookupColor(style.Diagonal)) : Color.Transparent; + diagDownColor = isBlackAndWhite ? Color.Black : diagDownColor; if (style.SuppressTop) topStyle = ExcelBorderStyle.None; if (style.SuppressBottom) bottomStyle = ExcelBorderStyle.None; From 4f659ce4eb39a7c44885a78b45d0cb2835c5c9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Fri, 16 Oct 2026 16:22:46 +0200 Subject: [PATCH 20/22] Added black and white support for jpegs --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 18 + .../DocumentObjects/PdfImageXObject.cs | 32 +- .../Helpers/JpegBlackWhiteHelper.cs | 82 ++-- .../Helpers/JpegCmykHelper.cs | 304 +++++++++++++ .../Helpers/JpegProgressiveHelper.cs | 430 ++++++++++++++++++ src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs | 2 +- 6 files changed, 831 insertions(+), 37 deletions(-) create mode 100644 src/EPPlus.Export.Pdf/Helpers/JpegCmykHelper.cs create mode 100644 src/EPPlus.Export.Pdf/Helpers/JpegProgressiveHelper.cs diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index c76546804..fea472176 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -1346,5 +1346,23 @@ public void PdfIsBlackAndWhiteTest1() string path = _pdfPath + "BlackAndWhiteTest1.pdf"; ws.SaveAsPdf(path); } + [TestMethod] + public void PdfIsBlackAndWhiteTest2() + { + using var p = OpenTemplatePackage("CMYK.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.BlackAndWhite = true; + string path = _pdfPath + "BlackAndWhiteTest2.pdf"; + ws.SaveAsPdf(path); + } + [TestMethod] + public void PdfIsBlackAndWhiteTest3() + { + using var p = OpenTemplatePackage("ProgressiveJPEG.xlsx"); + var ws = p.Workbook.Worksheets[0]; + ws.PrinterSettings.BlackAndWhite = true; + string path = _pdfPath + "BlackAndWhiteTest3.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs index 8a369d8ca..75ba824d3 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs @@ -56,20 +56,32 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, bool blackAndWhite, } else if (JpegDecoder.IsJpeg(imageBytes)) { - _bytes = blackAndWhite ? JpegBlackWhiteHelper.MakeGrayscale(imageBytes) : imageBytes; - Filter = "DCTDecode"; - BitsPerComponent = 8; - JpegDecoder.ReadJpegInfo(imageBytes, out int width, out int height, out int components, out bool adobe); - Width = width; - Height = height; - if (components == 4) + if (blackAndWhite && JpegCmykHelper.TryDecode(imageBytes, out int gw, out int gh, out byte[] grayPixels)) { - ColorSpace = "/DeviceCMYK"; - if (adobe) Decode = "[ 1 0 1 0 1 0 1 0 ]"; + Width = gw; + Height = gh; + BitsPerComponent = 8; + ColorSpace = "/DeviceGray"; + Filter = "FlateDecode"; + _bytes = PdfFlate.CompressLeaveOpen(grayPixels); } else { - ColorSpace = components == 1 ? "/DeviceGray" : "/DeviceRGB"; + JpegDecoder.ReadJpegInfo(imageBytes, out int width, out int height, out int components, out bool adobe); + _bytes = blackAndWhite ? JpegBlackWhiteHelper.MakeGrayscale(imageBytes) : imageBytes; + Filter = "DCTDecode"; + BitsPerComponent = 8; + Width = width; + Height = height; + if (components == 4) + { + ColorSpace = "/DeviceCMYK"; + if (adobe) Decode = "[ 1 0 1 0 1 0 1 0 ]"; + } + else + { + ColorSpace = components == 1 ? "/DeviceGray" : "/DeviceRGB"; + } } } else if (PngDecoder.IsPng(imageBytes)) diff --git a/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs index 93c1b054d..4ef2506ca 100644 --- a/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs +++ b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs @@ -22,10 +22,32 @@ internal static class JpegBlackWhiteHelper { internal static byte[] MakeGrayscale(byte[] jpeg) { - try { return TryTransform(jpeg) ?? jpeg; } + try + { + if (FindSofMarker(jpeg) == 0xC2) // progressive + return JpegProgressiveHelper.ToGrayBaseline(jpeg) ?? jpeg; + return TryTransform(jpeg) ?? jpeg; // your existing baseline/YCCK transcoder + } catch { return jpeg; } } + private static int FindSofMarker(byte[] d) + { + if (d == null || d.Length < 4 || (d[0] & 0xFF) != 0xFF || (d[1] & 0xFF) != 0xD8) return -1; + int i = 2; + while (i + 1 < d.Length) + { + if ((d[i] & 0xFF) != 0xFF) { i++; continue; } + int m = d[i + 1] & 0xFF; + if (m == 0xFF) { i++; continue; } + if (m == 0xD8 || m == 0xD9 || (m >= 0xD0 && m <= 0xD7)) { i += 2; continue; } + if (m >= 0xC0 && m <= 0xCF && m != 0xC4 && m != 0xC8 && m != 0xCC) return m; // SOFn + if (m == 0xDA) return -1; // reached the scan, no SOF found + i += 2 + (((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF)); + } + return -1; + } + private sealed class Huff { public byte[] Bits = new byte[17]; // Bits[1..16] = count of codes of that length @@ -115,7 +137,7 @@ public void Write(int code, int len) } } } - public void FlushToByte() { if (cnt > 0) Write(0x7F, 8 - cnt); } // pad with 1-bits (libjpeg-style) + public void FlushToByte() { if (cnt > 0) Write(0x7F, 8 - cnt); } private int rst; public void EmitRst() { FlushToByte(); Out.Add(0xFF); Out.Add((byte)(0xD0 + (rst & 7))); rst++; } } @@ -129,8 +151,8 @@ private static byte[] TryTransform(byte[] d) var cid = new int[4]; var cH = new int[4]; var cV = new int[4]; int W = 0, H = 0, Hmax = 1, Vmax = 1, dri = 0; int sosEnd = -1, scanTermMarker = -1; - // SOS component -> (sofIndex, Td, Ta) int sNs = 0; var sSof = new int[4]; var sTd = new int[4]; var sTa = new int[4]; + int adobeTransform = -1; int i = 2; while (i + 1 < d.Length) @@ -138,18 +160,18 @@ private static byte[] TryTransform(byte[] d) if ((d[i] & 0xFF) != 0xFF) { i++; continue; } int m = d[i + 1] & 0xFF; if (m == 0xFF) { i++; continue; } - if (m == 0xD9) break; // EOI + if (m == 0xD9) break; if (m == 0xC2 || m == 0xC3 || (m >= 0xC5 && m <= 0xCB) || (m >= 0xCD && m <= 0xCF)) - return null; // progressive / arithmetic / unsupported + return null; // progressive / arithmetic / unsupported int seg = ((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF); int p = i + 4; - if (m == 0xC0 || m == 0xC1) // SOF0 / SOF1 (baseline / ext. sequential) + if (m == 0xC0 || m == 0xC1) // SOF0 / SOF1 (baseline / ext. sequential) { H = ((d[p + 1] & 0xFF) << 8) | (d[p + 2] & 0xFF); W = ((d[p + 3] & 0xFF) << 8) | (d[p + 4] & 0xFF); comps = d[p + 5] & 0xFF; - if (comps != 3) return null; // only YCbCr handled here + if (comps != 3 && comps != 4) return null; for (int cc = 0; cc < comps; cc++) { int q = p + 6 + cc * 3; @@ -160,7 +182,13 @@ private static byte[] TryTransform(byte[] d) if (cV[cc] > Vmax) Vmax = cV[cc]; } } - else if (m == 0xC4) // DHT (may hold several tables) + else if (m == 0xEE && seg >= 14 && + d[p] == (byte)'A' && d[p + 1] == (byte)'d' && d[p + 2] == (byte)'o' && + d[p + 3] == (byte)'b' && d[p + 4] == (byte)'e') + { + adobeTransform = d[p + 11] & 0xFF; + } + else if (m == 0xC4) // DHT (may hold several tables) { int q = p, segEnd = i + 2 + seg; while (q < segEnd) @@ -174,8 +202,8 @@ private static byte[] TryTransform(byte[] d) if (tc == 0) dcT[th] = t; else acT[th] = t; } } - else if (m == 0xDD) { dri = ((d[p] & 0xFF) << 8) | (d[p + 1] & 0xFF); } // DRI - else if (m == 0xDA) // SOS + else if (m == 0xDD) { dri = ((d[p] & 0xFF) << 8) | (d[p + 1] & 0xFF); } // DRI + else if (m == 0xDA) // SOS { sNs = d[p] & 0xFF; for (int s = 0; s < sNs; s++) @@ -185,8 +213,7 @@ private static byte[] TryTransform(byte[] d) int sof = 0; for (int cc = 0; cc < comps; cc++) if (cid[cc] == selId) sof = cc; sSof[s] = sof; sTd[s] = (tdta >> 4) & 0xF; sTa[s] = tdta & 0xF; } - sosEnd = p + 1 + sNs * 2 + 3; // after Ss,Se,Ah/Al - // find the marker that terminates the entropy stream + sosEnd = p + 1 + sNs * 2 + 3; // after Ss, Se, Ah/Al int j = sosEnd; while (j + 1 < d.Length) { @@ -203,7 +230,10 @@ private static byte[] TryTransform(byte[] d) } if (sosEnd < 0 || scanTermMarker < 0 || W == 0 || H == 0) return null; - if (sNs != 3) return null; + + bool threeYcbcr = comps == 3 && sNs == 3 && adobeTransform != 0; + bool fourYcck = comps == 4 && sNs == 4 && adobeTransform == 2; // transform 0 on 3 comps = RGB, leave alone + if (!threeYcbcr && !fourYcck) return null; var br = new BitReader(d, sosEnd, scanTermMarker); var bw = new BitWriter(); @@ -220,15 +250,15 @@ private static byte[] TryTransform(byte[] d) { var dc = dcT[sTd[s]]; var ac = acT[sTa[s]]; if (dc == null || ac == null) return null; - bool luma = sSof[s] == 0; - int blocks = cH[sSof[s]] * cV[sSof[s]]; - + bool passthrough = sSof[s] != 1 && sSof[s] != 2; + int blocks = cH[sSof[s]] * cV[sSof[s]]; // zero only chroma (indices 1,2); + // keep Y (0) and, for YCCK, K (3) for (int b = 0; b < blocks; b++) { // ---- DC ---- int t = Decode(br, dc); int dcBits = br.Bits(t); - if (luma) + if (passthrough) { if (dc.EncLen[t] == 0) return null; bw.Write(dc.EncCode[t], dc.EncLen[t]); @@ -242,12 +272,12 @@ private static byte[] TryTransform(byte[] d) int run = rs >> 4, size = rs & 0xF; if (size == 0) { - if (luma) { if (ac.EncLen[rs] == 0) return null; bw.Write(ac.EncCode[rs], ac.EncLen[rs]); } - if (run != 15) break; // EOB - k += 16; continue; // ZRL + if (passthrough) { if (ac.EncLen[rs] == 0) return null; bw.Write(ac.EncCode[rs], ac.EncLen[rs]); } + if (run != 15) break; // EOB + k += 16; continue; // ZRL } int acBits = br.Bits(size); - if (luma) + if (passthrough) { if (ac.EncLen[rs] == 0) return null; bw.Write(ac.EncCode[rs], ac.EncLen[rs]); @@ -256,11 +286,11 @@ private static byte[] TryTransform(byte[] d) k += run + 1; } - if (!luma) // emit one neutral (zero) chroma block + if (!passthrough) { if (dc.EncLen[0] == 0 || ac.EncLen[0] == 0) return null; - bw.Write(dc.EncCode[0], dc.EncLen[0]); // DC category 0 (diff 0) - bw.Write(ac.EncCode[0], ac.EncLen[0]); // AC EOB + bw.Write(dc.EncCode[0], dc.EncLen[0]); // DC category 0 (diff 0) + bw.Write(ac.EncCode[0], ac.EncLen[0]); // AC EOB } } } @@ -269,8 +299,8 @@ private static byte[] TryTransform(byte[] d) bw.FlushToByte(); var outp = new List(d.Length); - for (int k = 0; k < sosEnd; k++) outp.Add(d[k]); // headers + SOS header, verbatim - outp.AddRange(bw.Out); // re-encoded entropy stream + for (int k = 0; k < sosEnd; k++) outp.Add(d[k]); // headers + SOS header, verbatim + outp.AddRange(bw.Out); // re-encoded entropy stream for (int k = scanTermMarker; k < d.Length; k++) outp.Add(d[k]); // EOI + trailer, verbatim return outp.ToArray(); } diff --git a/src/EPPlus.Export.Pdf/Helpers/JpegCmykHelper.cs b/src/EPPlus.Export.Pdf/Helpers/JpegCmykHelper.cs new file mode 100644 index 000000000..ad2435e09 --- /dev/null +++ b/src/EPPlus.Export.Pdf/Helpers/JpegCmykHelper.cs @@ -0,0 +1,304 @@ +/************************************************************************************************* + 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 System; +using System.Collections.Generic; + +namespace EPPlus.Export.Pdf.Helpers +{ + internal class JpegCmykHelper + { + internal static bool TryDecode(byte[] jpeg, out int width, out int height, out byte[] gray) + { + width = 0; height = 0; gray = null; + try { return Decode(jpeg, out width, out height, out gray); } + catch { return false; } + } + + private static readonly int[] ZZ = { + 0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28, + 35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63 }; + + private static readonly double[,] CC = BuildIdctBasis(); + private static double[,] BuildIdctBasis() + { + var c = new double[8, 8]; + for (int u = 0; u < 8; u++) + { + double cu = u == 0 ? Math.Sqrt(1.0 / 8.0) : Math.Sqrt(2.0 / 8.0); + for (int x = 0; x < 8; x++) c[u, x] = cu * Math.Cos((2 * x + 1) * u * Math.PI / 16.0); + } + return c; + } + + private sealed class HDec + { + public int[] MinCode = new int[17], MaxCode = new int[17], ValPtr = new int[17]; + public byte[] Vals; + public static HDec Build(byte[] bits1to16, byte[] vals) + { + var h = new HDec { Vals = vals }; + var sizes = new List(); + for (int l = 1; l <= 16; l++) for (int n = 0; n < bits1to16[l]; n++) sizes.Add(l); + var codes = new int[sizes.Count]; + int code = 0, k = 0; + if (sizes.Count > 0) + { + int si = sizes[0]; + while (k < sizes.Count) + { + while (k < sizes.Count && sizes[k] == si) { codes[k] = code; code++; k++; } + code <<= 1; si++; + } + } + k = 0; + for (int l = 1; l <= 16; l++) + { + if (bits1to16[l] == 0) h.MaxCode[l] = -1; + else { h.ValPtr[l] = k; h.MinCode[l] = codes[k]; k += bits1to16[l]; h.MaxCode[l] = codes[k - 1]; } + } + return h; + } + } + + private sealed class BitR + { + private readonly byte[] d; private int pos; private readonly int end; private int buf, cnt; + public BitR(byte[] data, int start, int stop) { d = data; pos = start; end = stop; } + public int Bit() + { + if (cnt == 0) + { + int b = pos < end ? (d[pos] & 0xFF) : 0; pos++; + if (b == 0xFF && pos < d.Length && (d[pos] & 0xFF) == 0x00) pos++; + buf = b; cnt = 8; + } + cnt--; return (buf >> cnt) & 1; + } + public int Bits(int n) { int v = 0; while (n-- > 0) v = (v << 1) | Bit(); return v; } + public void Align() { cnt = 0; } + } + + private sealed class Comp { public int Id, H, V, Q, Pred; public double[] Plane; public int Pw, Ph; } + + private static int Extend(int v, int s) => v >= (1 << (s - 1)) ? v : v - (1 << s) + 1; + + private static int Decode(BitR br, HDec t) + { + int code = br.Bit(), l = 1; + while (l <= 16 && (t.MaxCode[l] < 0 || code > t.MaxCode[l])) { code = (code << 1) | br.Bit(); l++; } + if (l > 16) return 0; + return t.Vals[t.ValPtr[l] + code - t.MinCode[l]] & 0xFF; + } + + private static bool Decode(byte[] d, out int width, out int height, out byte[] gray) + { + width = 0; height = 0; gray = null; + if (d == null || d.Length < 4 || (d[0] & 0xFF) != 0xFF || (d[1] & 0xFF) != 0xD8) return false; + + var qt = new int[4][]; + var dc = new HDec[4]; var ac = new HDec[4]; + var comps = new List(); + int W = 0, H = 0, dri = 0, adobeTransform = -1; bool adobeSeen = false; + int scanStart = -1, scanEnd = -1; + var sel = new List<(int sof, int td, int ta)>(); + + int i = 2; + while (i < d.Length - 1) + { + if ((d[i] & 0xFF) != 0xFF) { i++; continue; } + int m = d[i + 1] & 0xFF; + if (m == 0xFF) { i++; continue; } + if (m == 0xD8) { i += 2; continue; } + if (m == 0xD9) break; + int seg = ((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF); + int p = i + 4, segEnd = i + 2 + seg; + + if (m == 0xC0 || m == 0xC1) // baseline / ext-sequential only + { + H = ((d[p + 1] & 0xFF) << 8) | (d[p + 2] & 0xFF); + W = ((d[p + 3] & 0xFF) << 8) | (d[p + 4] & 0xFF); + int nc = d[p + 5] & 0xFF; + if (nc != 4) return false; // only CMYK here + for (int c = 0; c < nc; c++) + { + int q = p + 6 + c * 3; + comps.Add(new Comp { Id = d[q] & 0xFF, H = (d[q + 1] >> 4) & 0xF, V = d[q + 1] & 0xF, Q = d[q + 2] & 0xFF }); + } + } + else if (m == 0xC2 || m == 0xC3 || (m >= 0xC5 && m <= 0xCB) || (m >= 0xCD && m <= 0xCF)) + return false; // progressive / arithmetic: not handled + else if (m == 0xDB) + { + int q = p; + while (q < segEnd) + { + int pq = (d[q] >> 4) & 0xF, tq = d[q] & 0xF; q++; + var tbl = new int[64]; + if (pq == 0) { for (int n = 0; n < 64; n++) tbl[n] = d[q++] & 0xFF; } + else { for (int n = 0; n < 64; n++) { tbl[n] = ((d[q] & 0xFF) << 8) | (d[q + 1] & 0xFF); q += 2; } } + qt[tq] = tbl; + } + } + else if (m == 0xC4) + { + int q = p; + while (q < segEnd) + { + int tc = (d[q] >> 4) & 0xF, th = d[q] & 0xF; q++; + var bits = new byte[17]; int tot = 0; + for (int l = 1; l <= 16; l++) { bits[l] = d[q++]; tot += bits[l]; } + var vals = new byte[tot]; + for (int v = 0; v < tot; v++) vals[v] = d[q++]; + if (tc == 0) dc[th] = HDec.Build(bits, vals); else ac[th] = HDec.Build(bits, vals); + } + } + else if (m == 0xDD) { dri = ((d[p] & 0xFF) << 8) | (d[p + 1] & 0xFF); if (dri > 0) return false; } // restart: bail (rare for CMYK) + else if (m == 0xEE && seg >= 14 && + d[p] == (byte)'A' && d[p + 1] == (byte)'d' && d[p + 2] == (byte)'o' && d[p + 3] == (byte)'b' && d[p + 4] == (byte)'e') + { + adobeSeen = true; adobeTransform = d[p + 11] & 0xFF; + } + else if (m == 0xDA) + { + int ns = d[p] & 0xFF; + for (int s = 0; s < ns; s++) + { + int cid = d[p + 1 + s * 2] & 0xFF, tdta = d[p + 2 + s * 2] & 0xFF; + int sof = 0; for (int c = 0; c < comps.Count; c++) if (comps[c].Id == cid) sof = c; + sel.Add((sof, (tdta >> 4) & 0xF, tdta & 0xF)); + } + scanStart = p + 1 + ns * 2 + 3; + int j = scanStart; + while (j < d.Length - 1) + { + if ((d[j] & 0xFF) == 0xFF) + { + int mm = d[j + 1] & 0xFF; + if (mm != 0x00 && !(mm >= 0xD0 && mm <= 0xD7)) break; + } + j++; + } + scanEnd = j; break; + } + i = segEnd; + } + + if (W == 0 || H == 0 || comps.Count != 4 || scanStart < 0) return false; + if (adobeTransform == 2) return false; // YCCK -> handled by the transcoder + + int Hmax = 1, Vmax = 1; + foreach (var c in comps) { if (c.H > Hmax) Hmax = c.H; if (c.V > Vmax) Vmax = c.V; } + int mcuW = (W + 8 * Hmax - 1) / (8 * Hmax); + int mcuH = (H + 8 * Vmax - 1) / (8 * Vmax); + foreach (var c in comps) + { + c.Pw = mcuW * c.H * 8; c.Ph = mcuH * c.V * 8; + c.Plane = new double[c.Pw * c.Ph]; + } + + var br = new BitR(d, scanStart, scanEnd); + var coef = new double[64]; var nat = new double[64]; var spa = new double[64]; + int mcuCount = 0; + for (int my = 0; my < mcuH; my++) + for (int mx = 0; mx < mcuW; mx++) + { + if (dri > 0 && mcuCount > 0 && mcuCount % dri == 0) + { + br.Align(); + // skip RSTn is implicit: BitR stops at end; markers inside handled by re-seek is omitted + foreach (var c in comps) c.Pred = 0; + } + foreach (var (sof, td, ta) in sel) + { + var c = comps[sof]; + for (int v = 0; v < c.V; v++) + for (int h = 0; h < c.H; h++) + { + Array.Clear(coef, 0, 64); + int s = Decode(br, dc[td]); + int diff = s != 0 ? Extend(br.Bits(s), s) : 0; + c.Pred += diff; coef[0] = c.Pred; + int k = 1; + while (k < 64) + { + int rs = Decode(br, ac[ta]), r = rs >> 4, sz = rs & 0xF; + if (sz == 0) { if (r == 15) { k += 16; continue; } break; } + k += r; if (k > 63) break; + coef[k] = Extend(br.Bits(sz), sz); k++; + } + var q = qt[c.Q]; + Array.Clear(nat, 0, 64); + for (int zz = 0; zz < 64; zz++) nat[ZZ[zz]] = coef[zz] * q[zz]; + Idct8x8(nat, spa); + int by = (my * c.V + v) * 8, bx = (mx * c.H + h) * 8; + for (int yy = 0; yy < 8; yy++) + { + int row = (by + yy) * c.Pw + bx; + for (int xx = 0; xx < 8; xx++) c.Plane[row + xx] = spa[yy * 8 + xx] + 128.0; + } + } + } + mcuCount++; + } + + // assemble gray + gray = new byte[W * H]; + var Cp = comps[0]; var Mp = comps[1]; var Yp = comps[2]; var Kp = comps[3]; + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + { + double cv = Sample(Cp, x, y, Hmax, Vmax); + double mv = Sample(Mp, x, y, Hmax, Vmax); + double yv = Sample(Yp, x, y, Hmax, Vmax); + double kv = Sample(Kp, x, y, Hmax, Vmax); + if (adobeSeen) { cv = 255 - cv; mv = 255 - mv; yv = 255 - yv; kv = 255 - kv; } + double r = (255 - cv) * (255 - kv) / 255.0; + double g = (255 - mv) * (255 - kv) / 255.0; + double b = (255 - yv) * (255 - kv) / 255.0; + int lum = (int)(0.2126 * r + 0.7152 * g + 0.0722 * b + 0.5); + gray[y * W + x] = (byte)(lum < 0 ? 0 : lum > 255 ? 255 : lum); + } + width = W; height = H; + return true; + } + + private static double Sample(Comp c, int x, int y, int Hmax, int Vmax) + { + int sx = x * c.H / Hmax, sy = y * c.V / Vmax; // nearest-neighbour upsample + if (sx >= c.Pw) sx = c.Pw - 1; + if (sy >= c.Ph) sy = c.Ph - 1; + double v = c.Plane[sy * c.Pw + sx]; + return v < 0 ? 0 : v > 255 ? 255 : v; + } + + private static void Idct8x8(double[] f, double[] outp) + { + // spatial = C^T * F * C, with F stored row-major natural order + var tmp = new double[64]; + for (int x = 0; x < 8; x++) // tmp = C^T * F + for (int j = 0; j < 8; j++) + { + double sum = 0; + for (int u = 0; u < 8; u++) sum += CC[u, x] * f[u * 8 + j]; + tmp[x * 8 + j] = sum; + } + for (int xr = 0; xr < 8; xr++) // out = tmp * C + for (int xc = 0; xc < 8; xc++) + { + double sum = 0; + for (int v = 0; v < 8; v++) sum += tmp[xr * 8 + v] * CC[v, xc]; + outp[xr * 8 + xc] = sum; + } + } + } +} diff --git a/src/EPPlus.Export.Pdf/Helpers/JpegProgressiveHelper.cs b/src/EPPlus.Export.Pdf/Helpers/JpegProgressiveHelper.cs new file mode 100644 index 000000000..88c72bf62 --- /dev/null +++ b/src/EPPlus.Export.Pdf/Helpers/JpegProgressiveHelper.cs @@ -0,0 +1,430 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.Pdf.Helpers +{ + internal class JpegProgressiveHelper + { + internal static byte[] ToGrayBaseline(byte[] jpeg) + { + try { return Transform(jpeg); } + catch { return null; } + } + + // ---- standard Annex K.3 Huffman tables (BITS[16] + VALUES) ---- + private static readonly byte[] DcLumBits = { 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 }; // [0] unused, 1..16 + private static readonly byte[] DcChrBits = { 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 }; + private static readonly byte[] DcVals = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + private static readonly byte[] AcLumBits = { 0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d }; + private static readonly byte[] AcChrBits = { 0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 }; + private static readonly byte[] AcLumVals = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08,0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa }; + private static readonly byte[] AcChrVals = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91,0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa }; + + private sealed class HDec + { + public int[] MinCode = new int[17]; + public int[] MaxCode = new int[17]; + public int[] ValPtr = new int[17]; + public byte[] Vals; + public static HDec Build(byte[] bits1to16, byte[] vals) // bits indexed 1..16 + { + var h = new HDec { Vals = vals }; + var sizes = new List(); + for (int l = 1; l <= 16; l++) for (int i = 0; i < bits1to16[l]; i++) sizes.Add(l); + var codes = new int[sizes.Count]; + int code = 0, k = 0; + if (sizes.Count > 0) + { + int si = sizes[0]; + while (k < sizes.Count) + { + while (k < sizes.Count && sizes[k] == si) { codes[k] = code; code++; k++; } + code <<= 1; si++; + } + } + k = 0; + for (int l = 1; l <= 16; l++) + { + if (bits1to16[l] == 0) { h.MaxCode[l] = -1; } + else { h.ValPtr[l] = k; h.MinCode[l] = codes[k]; k += bits1to16[l]; h.MaxCode[l] = codes[k - 1]; } + } + return h; + } + } + + private sealed class HEnc + { + public int[] Code = new int[256]; + public int[] Len = new int[256]; + public static HEnc Build(byte[] bits1to16, byte[] vals) + { + var e = new HEnc(); + var sizes = new List(); + for (int l = 1; l <= 16; l++) for (int i = 0; i < bits1to16[l]; i++) sizes.Add(l); + int code = 0, k = 0; + if (sizes.Count > 0) + { + int si = sizes[0]; + while (k < sizes.Count) + { + while (k < sizes.Count && sizes[k] == si) { e.Code[vals[k]] = code; e.Len[vals[k]] = si; code++; k++; } + code <<= 1; si++; + } + } + return e; + } + } + + private sealed class BitR + { + private readonly byte[] d; private int pos; private readonly int end; + private int buf, cnt; + public BitR(byte[] data, int start, int stop) { d = data; pos = start; end = stop; } + public int Bit() + { + if (cnt == 0) + { + int b = pos < end ? (d[pos] & 0xFF) : 0; pos++; + if (b == 0xFF && pos < d.Length && (d[pos] & 0xFF) == 0x00) pos++; + buf = b; cnt = 8; + } + cnt--; return (buf >> cnt) & 1; + } + public int Bits(int n) { int v = 0; while (n-- > 0) v = (v << 1) | Bit(); return v; } + } + + private sealed class Comp { public int Id, H, V, Q, Bpl, Bpc, WB, HB, Pred; public int[] Coef; } + + private static int Extend(int v, int s) => v >= (1 << (s - 1)) ? v : v - (1 << s) + 1; + + private static int Decode(BitR br, HDec t) + { + int code = br.Bit(), l = 1; + while (l <= 16 && (t.MaxCode[l] < 0 || code > t.MaxCode[l])) { code = (code << 1) | br.Bit(); l++; } + if (l > 16) return 0; + return t.Vals[t.ValPtr[l] + code - t.MinCode[l]] & 0xFF; + } + + private static byte[] Transform(byte[] d) + { + if (d == null || d.Length < 4 || (d[0] & 0xFF) != 0xFF || (d[1] & 0xFF) != 0xD8) return null; + + int W = 0, H = 0, Hmax = 1, Vmax = 1, mcuW = 0, mcuH = 0, dri = 0; + var comps = new List(); + var dqtSegs = new List(); + var dc = new HDec[4]; var ac = new HDec[4]; + int eobrun = 0; + bool setup = false; + + int i = 2; + while (i < d.Length - 1) + { + if ((d[i] & 0xFF) != 0xFF) { i++; continue; } + int m = d[i + 1] & 0xFF; + if (m == 0xFF) { i++; continue; } + if (m == 0xD8) { i += 2; continue; } + if (m == 0xD9) break; + int seg = ((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF); + int p = i + 4, segEnd = i + 2 + seg; + + if (m == 0xC2) // SOF2 progressive + { + H = ((d[p + 1] & 0xFF) << 8) | (d[p + 2] & 0xFF); + W = ((d[p + 3] & 0xFF) << 8) | (d[p + 4] & 0xFF); + int nc = d[p + 5] & 0xFF; + if (nc != 3) return null; + for (int c = 0; c < nc; c++) + { + int q = p + 6 + c * 3; + comps.Add(new Comp { Id = d[q] & 0xFF, H = (d[q + 1] >> 4) & 0xF, V = d[q + 1] & 0xF, Q = d[q + 2] & 0xFF }); + } + Hmax = 1; Vmax = 1; + foreach (var c in comps) { if (c.H > Hmax) Hmax = c.H; if (c.V > Vmax) Vmax = c.V; } + mcuW = (W + 8 * Hmax - 1) / (8 * Hmax); + mcuH = (H + 8 * Vmax - 1) / (8 * Vmax); + foreach (var c in comps) + { + c.Bpl = mcuW * c.H; c.Bpc = mcuH * c.V; + c.WB = (W * c.H + 8 * Hmax - 1) / (8 * Hmax); + c.HB = (H * c.V + 8 * Vmax - 1) / (8 * Vmax); + c.Coef = new int[c.Bpl * c.Bpc * 64]; + } + setup = true; + } + else if (m == 0xC0 || m == 0xC1 || m == 0xC3 || (m >= 0xC5 && m <= 0xCB) || (m >= 0xCD && m <= 0xCF)) + { + return null; // not progressive Huffman — not our job + } + else if (m == 0xDB) { var s = new byte[segEnd - i]; System.Array.Copy(d, i, s, 0, s.Length); dqtSegs.Add(s); } + else if (m == 0xC4) + { + int q = p; + while (q < segEnd) + { + int tc = (d[q] >> 4) & 0xF, th = d[q] & 0xF; q++; + var bits = new byte[17]; int tot = 0; + for (int l = 1; l <= 16; l++) { bits[l] = d[q++]; tot += bits[l]; } + var vals = new byte[tot]; + for (int v = 0; v < tot; v++) vals[v] = d[q++]; + if (tc == 0) dc[th] = HDec.Build(bits, vals); else ac[th] = HDec.Build(bits, vals); + } + } + else if (m == 0xDD) { dri = ((d[p] & 0xFF) << 8) | (d[p + 1] & 0xFF); if (dri > 0) return null; } // progressive+restart: bail (rare) + else if (m == 0xDA) // SOS — one scan + { + if (!setup) return null; + int ns = d[p] & 0xFF; + var sel = new (int sof, int td, int ta)[ns]; + for (int s = 0; s < ns; s++) + { + int cid = d[p + 1 + s * 2] & 0xFF, tdta = d[p + 2 + s * 2] & 0xFF; + int sof = 0; for (int c = 0; c < comps.Count; c++) if (comps[c].Id == cid) sof = c; + sel[s] = (sof, (tdta >> 4) & 0xF, tdta & 0xF); + } + int ss = d[p + 1 + ns * 2] & 0xFF, se = d[p + 2 + ns * 2] & 0xFF; + int ahal = d[p + 3 + ns * 2] & 0xFF, ah = ahal >> 4, al = ahal & 0xF; + int dataStart = p + 4 + ns * 2, j = dataStart; + while (j < d.Length - 1) + { + if ((d[j] & 0xFF) == 0xFF) + { + int mm = d[j + 1] & 0xFF; + if (mm != 0x00 && !(mm >= 0xD0 && mm <= 0xD7)) break; + } + j++; + } + DecodeScan(comps, sel, ss, se, ah, al, dc, ac, d, dataStart, j, mcuW, mcuH, ref eobrun); + i = j; continue; + } + i = segEnd; + } + + if (!setup) return null; + return EncodeBaseline(comps, dqtSegs, W, H, mcuW, mcuH); + } + + private static void DecodeScan(List comps, (int sof, int td, int ta)[] sel, + int Ss, int Se, int Ah, int Al, HDec[] dc, HDec[] ac, byte[] d, int start, int end, + int mcuW, int mcuH, ref int eobrun) + { + var br = new BitR(d, start, end); + eobrun = 0; + foreach (var c in comps) c.Pred = 0; + + if (Ss == 0) // DC scan (interleaved over scan components) + { + for (int my = 0; my < mcuH; my++) + for (int mx = 0; mx < mcuW; mx++) + foreach (var (sof, td, ta) in sel) + { + var c = comps[sof]; + for (int v = 0; v < c.V; v++) + for (int h = 0; h < c.H; h++) + { + int bRow = my * c.V + v, bCol = mx * c.H + h; + int baseIdx = (bRow * c.Bpl + bCol) * 64; + if (Ah == 0) + { + int s = Decode(br, dc[td]); + int diff = s != 0 ? Extend(br.Bits(s), s) : 0; + c.Pred += diff; c.Coef[baseIdx] = c.Pred << Al; + } + else if (br.Bit() != 0) c.Coef[baseIdx] |= (1 << Al); + } + } + return; + } + + // AC scan — single component, non-interleaved + var comp = comps[sel[0].sof]; + var act = ac[sel[0].ta]; + int p1 = 1 << Al, m1 = (-1) << Al; + + for (int by = 0; by < comp.HB; by++) + for (int bx = 0; bx < comp.WB; bx++) + { + int baseIdx = (by * comp.Bpl + bx) * 64; + if (Ah == 0) // first pass + { + if (eobrun > 0) { eobrun--; continue; } + int k = Ss; + while (k <= Se) + { + int rs = Decode(br, act), r = rs >> 4, s = rs & 0xF; + if (s == 0) + { + if (r < 15) { eobrun = (1 << r) + (r != 0 ? br.Bits(r) : 0) - 1; break; } + k += 16; + } + else { k += r; comp.Coef[baseIdx + k] = Extend(br.Bits(s), s) << Al; k++; } + } + } + else // refinement pass + { + int k = Ss; + if (eobrun == 0) + { + while (k <= Se) + { + int rs = Decode(br, act), r = rs >> 4, s = rs & 0xF; + if (s != 0) { s = br.Bit() != 0 ? p1 : m1; } + else if (r != 15) { eobrun = (1 << r) + (r != 0 ? br.Bits(r) : 0); break; } + + while (true) + { + int ci = baseIdx + k; + if (comp.Coef[ci] != 0) + { + if (br.Bit() != 0 && (comp.Coef[ci] & p1) == 0) + comp.Coef[ci] += comp.Coef[ci] > 0 ? p1 : m1; + } + else { r--; if (r < 0) break; } + k++; if (k > Se) break; + } + if (s != 0 && k <= Se) comp.Coef[baseIdx + k] = s; + k++; + } + } + if (eobrun > 0) + { + while (k <= Se) + { + int ci = baseIdx + k; + if (comp.Coef[ci] != 0 && br.Bit() != 0 && (comp.Coef[ci] & p1) == 0) + comp.Coef[ci] += comp.Coef[ci] > 0 ? p1 : m1; + k++; + } + eobrun--; + } + } + } + } + + private sealed class BitW + { + public readonly List Out = new List(); + private int acc, cnt; + public void Write(int code, int len) + { + for (int b = len - 1; b >= 0; b--) + { + acc = (acc << 1) | ((code >> b) & 1); + if (++cnt == 8) { byte v = (byte)acc; Out.Add(v); if (v == 0xFF) Out.Add(0x00); acc = 0; cnt = 0; } + } + } + public void Flush() { if (cnt > 0) Write(0x7F, 8 - cnt); } + } + + private static int Cat(int x) { x = x < 0 ? -x : x; int n = 0; while (x != 0) { n++; x >>= 1; } return n; } + private static int Mag(int v, int s) { if (v < 0) v = v - 1 + (1 << s); return v & ((1 << s) - 1); } + + private static byte[] EncodeBaseline(List comps, List dqtSegs, int W, int H, int mcuW, int mcuH) + { + var dcL = HEnc.Build(DcLumBits, DcVals); var dcC = HEnc.Build(DcChrBits, DcVals); + var acL = HEnc.Build(AcLumBits, AcLumVals); var acC = HEnc.Build(AcChrBits, AcChrVals); + + var o = new List { 0xFF, 0xD8 }; + foreach (var s in dqtSegs) o.AddRange(s); + + // SOF0 + var sof = new List { 8, (byte)(H >> 8), (byte)H, (byte)(W >> 8), (byte)W, (byte)comps.Count }; + foreach (var c in comps) { sof.Add((byte)c.Id); sof.Add((byte)((c.H << 4) | c.V)); sof.Add((byte)c.Q); } + o.Add(0xFF); o.Add(0xC0); o.Add((byte)((sof.Count + 2) >> 8)); o.Add((byte)(sof.Count + 2)); o.AddRange(sof); + + // DHT (standard tables) + void Dht(int tc, int th, byte[] bits1to16, byte[] vals) + { + int len = 2 + 1 + 16 + vals.Length; + o.Add(0xFF); o.Add(0xC4); o.Add((byte)(len >> 8)); o.Add((byte)len); + o.Add((byte)((tc << 4) | th)); + for (int l = 1; l <= 16; l++) o.Add(bits1to16[l]); + o.AddRange(vals); + } + Dht(0, 0, DcLumBits, DcVals); Dht(0, 1, DcChrBits, DcVals); + Dht(1, 0, AcLumBits, AcLumVals); Dht(1, 1, AcChrBits, AcChrVals); + + // APP14 Adobe, transform = 1 (YCbCr) so a viewer keeps the YCbCr interpretation + o.Add(0xFF); o.Add(0xEE); o.Add(0x00); o.Add(0x0E); + o.AddRange(new byte[] { (byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e' }); + o.AddRange(new byte[] { 0, 100, 0, 0, 0, 0, 1 }); + + // SOS (baseline, interleaved, all components) + var sos = new List { (byte)comps.Count }; + for (int ci = 0; ci < comps.Count; ci++) { int t = ci == 0 ? 0 : 1; sos.Add((byte)comps[ci].Id); sos.Add((byte)((t << 4) | t)); } + sos.Add(0); sos.Add(63); sos.Add(0); + o.Add(0xFF); o.Add(0xDA); o.Add((byte)((sos.Count + 2) >> 8)); o.Add((byte)(sos.Count + 2)); o.AddRange(sos); + + var bw = new BitW(); + var pred = new int[comps.Count]; + for (int my = 0; my < mcuH; my++) + for (int mx = 0; mx < mcuW; mx++) + for (int ci = 0; ci < comps.Count; ci++) + { + var c = comps[ci]; + bool chroma = ci != 0; + var dct = chroma ? dcC : dcL; + var act = chroma ? acC : acL; + for (int v = 0; v < c.V; v++) + for (int h = 0; h < c.H; h++) + { + int baseIdx = ((my * c.V + v) * c.Bpl + (mx * c.H + h)) * 64; + int dcVal = chroma ? 0 : c.Coef[baseIdx]; + int diff = dcVal - pred[ci]; pred[ci] = dcVal; + int s = Cat(diff); + bw.Write(dct.Code[s], dct.Len[s]); + if (s != 0) bw.Write(Mag(diff, s), s); + + if (chroma) { bw.Write(act.Code[0x00], act.Len[0x00]); } // EOB — all AC zero + else + { + int run = 0; + for (int k = 1; k < 64; k++) + { + int cf = c.Coef[baseIdx + k]; + if (cf == 0) run++; + else + { + while (run > 15) { bw.Write(act.Code[0xF0], act.Len[0xF0]); run -= 16; } + int sc = Cat(cf); + int sym = (run << 4) | sc; + bw.Write(act.Code[sym], act.Len[sym]); + bw.Write(Mag(cf, sc), sc); + run = 0; + } + } + if (run > 0) bw.Write(act.Code[0x00], act.Len[0x00]); + } + } + } + bw.Flush(); + o.AddRange(bw.Out); + o.Add(0xFF); o.Add(0xD9); + return o.ToArray(); + } + } +} diff --git a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs index 5578bd6b1..43cc2fa97 100644 --- a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs +++ b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs @@ -172,7 +172,7 @@ internal static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, } var swap = prev; prev = cur; cur = swap; } - if (blackAndWhite && colorType == 6) // <-- raw RGB, BEFORE compression + if (blackAndWhite && colorType == 6) PdfImageXObject.GrayscaleRgb(color); deflatedColor = PdfFlate.CompressLeaveOpen(color); deflatedAlpha = PdfFlate.CompressLeaveOpen(alpha); From cb472d2df224ed443ef254c8954129850fb76001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Mon, 19 Oct 2026 11:22:08 +0200 Subject: [PATCH 21/22] removed comments --- src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs index 4ef2506ca..d5679c9a9 100644 --- a/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs +++ b/src/EPPlus.Export.Pdf/Helpers/JpegBlackWhiteHelper.cs @@ -26,7 +26,7 @@ internal static byte[] MakeGrayscale(byte[] jpeg) { if (FindSofMarker(jpeg) == 0xC2) // progressive return JpegProgressiveHelper.ToGrayBaseline(jpeg) ?? jpeg; - return TryTransform(jpeg) ?? jpeg; // your existing baseline/YCCK transcoder + return TryTransform(jpeg) ?? jpeg; } catch { return jpeg; } } From 98fc8b3683f6451df32c1f04fef5b53fc37df74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 20 Oct 2026 09:22:01 +0200 Subject: [PATCH 22/22] Fixed content not centering properly when scaling the document. --- src/EPPlus/Export/PdfExport/Data/PageData.cs | 1 + src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/EPPlus/Export/PdfExport/Data/PageData.cs b/src/EPPlus/Export/PdfExport/Data/PageData.cs index c8c48b1bd..3494734f9 100644 --- a/src/EPPlus/Export/PdfExport/Data/PageData.cs +++ b/src/EPPlus/Export/PdfExport/Data/PageData.cs @@ -44,6 +44,7 @@ internal struct Page public double HeadingHeight; public double UsedWidth; public double UsedHeight; + public bool IsCommentsPage; } internal struct Pages diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index dacd5e548..017176460 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -1078,6 +1078,9 @@ internal static List GetPages(PdfPageSettings[] sheetSettings, PdfWorkshe pages.IsCommentsPage = true; pages.Settings = pageSettings; pages.SheetIndex = si; + pages.IsCommentsPage = true; + for (int k = 0; k < pages.Page.Length; k++) + pages.Page[k].IsCommentsPage = true; PagesCollection.Add(pages); } } @@ -2175,8 +2178,8 @@ private static void EmitBandFrameV(List target, PdfRange range, double internal static double GetOriginX(PdfPageSettings pageSettings, Page page) { if (!pageSettings.CenterOnPageHorizontally) return pageSettings.ContentBounds.Left; - - var offset = (pageSettings.ContentBounds.Width - page.UsedWidth) / 2d; + var contentWidth = page.IsCommentsPage ? pageSettings.ContentBounds.Width : pageSettings.EffectiveContentWidth; + var offset = (contentWidth - page.UsedWidth) / 2d; return pageSettings.ContentBounds.Left + Math.Max(0d, offset); } @@ -2186,8 +2189,8 @@ internal static double GetOriginX(PdfPageSettings pageSettings, Page page) internal static double GetOriginY(PdfPageSettings pageSettings, Page page) { if (!pageSettings.CenterOnPageVertically) return pageSettings.ContentBounds.Top; - - var offset = (pageSettings.ContentBounds.Height - page.UsedHeight) / 2d; + var contentHeight = page.IsCommentsPage ? pageSettings.ContentBounds.Height : pageSettings.EffectiveContentHeight; + var offset = (contentHeight - page.UsedHeight) / 2d; return pageSettings.ContentBounds.Top - Math.Max(0d, offset); }