Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/EPPlus.Export.Pdf.Tests/PdfTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,10 +12,15 @@ Date Author Change
*************************************************************************************************/
using EPPlus.Export.Pdf.Settings;
using EPPlus.Export.Pdf.Tests;
using EPPlus.Export.Pdf.Settings.PdfPageSizes;
using OfficeOpenXml;
using OfficeOpenXml.Export.PdfExport;
using OfficeOpenXml.Export.PdfExport.Settings;
using OfficeOpenXml.Style;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace EPPlusTest.PDF
{
Expand DownExpand Up@@ -637,5 +642,122 @@ public void EPPlusToPdf()
p.Workbook.SaveAsPdf(_pdfPath + "Snake.Pdf");
p.SaveAs(_pdfPath + "Snake.xlsx");
}

[TestMethod]
public void Testing()
{
using var p = OpenTemplatePackage("PDFTestKarl.xlsx");
var wb = p.Workbook;
string path = _pdfPath + "WorksheetTest1.pdf";
wb.SaveAsPdf(path);
AssertLooksLikePdf(File.ReadAllBytes(path));
}

[TestMethod]
public void EachWorksheetUsesItsOwnOrientation()
{
using (var package = OpenTemplatePackage("PDFTestKarl.xlsx"))
{
package.Workbook.Worksheets[0].PrinterSettings.Orientation = eOrientation.Portrait;
package.Workbook.Worksheets[1].PrinterSettings.Orientation = eOrientation.Landscape;

var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(
package.Workbook,
package.Workbook.Worksheets[0].PrinterSettings);

byte[] pdf;
using (var ms = new MemoryStream())
{
new PdfCatalog(ms, settings, package.Workbook);
pdf = ms.ToArray();
}

var matches = Regex.Matches(
Encoding.ASCII.GetString(pdf),
@"/MediaBox\s*\[\s*0\s+0\s+(?<w>[\d.]+)\s+(?<h>[\d.]+)\s*\]");

Assert.AreEqual(2, matches.Count, "Expected one page per worksheet.");

var ci = CultureInfo.InvariantCulture;
double w1 = double.Parse(matches[0].Groups["w"].Value, ci);
double h1 = double.Parse(matches[0].Groups["h"].Value, ci);
double w2 = double.Parse(matches[1].Groups["w"].Value, ci);
double h2 = double.Parse(matches[1].Groups["h"].Value, ci);

Assert.IsTrue(h1 > w1, "Page 1 should be portrait.");
Assert.IsTrue(w2 > h2, "Page 2 should be landscape.");
// Landscape is the same paper transposed, not a different paper size.
Assert.AreEqual(w1, h2, 0.01d);
Assert.AreEqual(h1, w2, 0.01d);
}
}

[TestMethod]
public void EachWorksheetUsesItsOwnShowGridLines()
{
using (var package = OpenTemplatePackage("PDFTestKarl.xlsx"))
{
package.Workbook.Worksheets[0].PrinterSettings.ShowGridLines = false;
package.Workbook.Worksheets[1].PrinterSettings.ShowGridLines = true;

var baseSettings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(
package.Workbook,
package.Workbook.Worksheets[0].PrinterSettings);

var s0 = GetPdfSettings.GetPdfSettingsForSheet(
baseSettings, package.Workbook.Worksheets[0].PrinterSettings);
var s1 = GetPdfSettings.GetPdfSettingsForSheet(
baseSettings, package.Workbook.Worksheets[1].PrinterSettings);

Assert.IsFalse(s0.ShowGridLines, "Sheet 1 did not ask for gridlines.");
Assert.IsTrue(s1.ShowGridLines, "Sheet 2 asked for gridlines.");
Assert.IsFalse(baseSettings.ShowGridLines, "The base object must not be mutated.");
}
}

[TestMethod]
public void EachWorksheetUsesItsOwnPaperSize()
{
using (var package = OpenTemplatePackage("PDFTestKarl.xlsx"))
{
// Orientation is set explicitly so a transposed page size cannot be
// mistaken for a different paper size.
package.Workbook.Worksheets[0].PrinterSettings.Orientation = eOrientation.Portrait;
package.Workbook.Worksheets[1].PrinterSettings.Orientation = eOrientation.Portrait;
package.Workbook.Worksheets[0].PrinterSettings.PaperSize = ePaperSize.A4;
package.Workbook.Worksheets[1].PrinterSettings.PaperSize = ePaperSize.A3;

var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings(
package.Workbook,
package.Workbook.Worksheets[0].PrinterSettings);

byte[] pdf;
using (var ms = new MemoryStream())
{
new PdfCatalog(ms, settings, package.Workbook);
pdf = ms.ToArray();
}

var matches = Regex.Matches(
Encoding.ASCII.GetString(pdf),
@"/MediaBox\s*\[\s*0\s+0\s+(?<w>[\d.]+)\s+(?<h>[\d.]+)\s*\]");

Assert.AreEqual(2, matches.Count, "Expected one page per worksheet.");

var ci = CultureInfo.InvariantCulture;
double w1 = double.Parse(matches[0].Groups["w"].Value, ci);
double h1 = double.Parse(matches[0].Groups["h"].Value, ci);
double w2 = double.Parse(matches[1].Groups["w"].Value, ci);
double h2 = double.Parse(matches[1].Groups["h"].Value, ci);

// Compare against the source of truth rather than literal point values.
// PdfPageSize rounds mm to whole points, so 210x297 mm becomes 595x842.
Assert.AreEqual(PdfPageSize.A4.WidthPu, w1, "Page 1 should be A4.");
Assert.AreEqual(PdfPageSize.A4.HeightPu, h1, "Page 1 should be A4.");
Assert.AreEqual(PdfPageSize.A3.WidthPu, w2, "Page 2 should be A3, not sheet 1's A4.");
Assert.AreEqual(PdfPageSize.A3.HeightPu, h2, "Page 2 should be A3, not sheet 1's A4.");
}
}

}
}
36 changes: 20 additions & 16 deletions src/EPPlus.Export.Pdf/ExcelPdf.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@ namespace EPPlus.Export.Pdf
internal class ExcelPdf
{
private PdfPageSettings _pageSettings;
private PdfDocumentSettings _documentSettings;
private PdfDictionaries _dictionaries;
internal List<PdfObject> _document = new List<PdfObject>();
private string _debugString;
Expand DownExpand Up@@ -69,8 +70,8 @@ private string GetPatternLabel(PdfCellLayout layout)

//Add Fonts //Need to update this method a bit. We should check for all default fonts and not only courier new? Also need to check if we are allowed to embedd the font.
internal void AddFontData()
{
if (_pageSettings.EmbeddFonts)
{
if (_documentSettings.EmbeddFonts)
{
foreach (var font in _dictionaries.Fonts)
{
Expand DownExpand Up@@ -144,8 +145,10 @@ private PdfCatalog AddCatalog(int pagesObjectNumber)
}

//Create Content
private void AddContent(Transform pageLayout, PdfPage page)
private void AddContent(PdfPageLayout pageLayout, PdfPage page)
{
var pageSettings = pageLayout.Settings;

var cells = pageLayout.ChildObjects.Where(t =>
(t is PdfCellLayout || t is PdfCellContentLayout || t is PdfCellBorderLayout) &&
!(t is PdfCellLayout cc && (cc.IsHeading || cc.IsPrintTitle)) &&
Expand All@@ -160,7 +163,7 @@ private void AddContent(Transform pageLayout, PdfPage page)
//Add clipping rectangle around page content.
contentStream.AddCommand("q");
contentStream.AddMarginClipping((PdfPageLayout)pageLayout);
if (_pageSettings.ShowGridLines)
if (pageSettings.ShowGridLines)
{
contentStream.AddInnerGridLines(pageLayout);
}
Expand All@@ -172,7 +175,7 @@ private void AddContent(Transform pageLayout, PdfPage page)
foreach (PdfCellContentLayout content in cells.OfType<PdfCellContentLayout>())
{
contentStream.AddCommand($"% CELL TEXT : {content.Name}");
contentStream.AddCellContentLayout(content, _dictionaries, _pageSettings);
contentStream.AddCellContentLayout(content, _dictionaries, pageSettings);
}
foreach (PdfCellBorderLayout border in cells.OfType<PdfCellBorderLayout>())
{
Expand All@@ -191,20 +194,20 @@ private void AddContent(Transform pageLayout, PdfPage page)
case PdfCellLayout layout:
contentStream.AddCellLayout(layout, GetPatternLabel(layout)); break;
case PdfCellContentLayout contentLayout:
contentStream.AddCellContentLayout(contentLayout, _dictionaries, _pageSettings); break;
contentStream.AddCellContentLayout(contentLayout, _dictionaries, pageSettings); break;
case PdfCellBorderLayout borderLayout:
contentStream.AddBorderLayout(borderLayout); break;
}
}
if (_pageSettings.ShowGridLines || _pageSettings.ShowHeadings)
if (pageSettings.ShowGridLines || pageSettings.ShowHeadings)
{
contentStream.AddOuterGridBorder(pageLayout);
contentStream.AddPrintTitleGridLines(pageLayout);
}
//Add header and footer.
foreach (var hf in headerFooterLayouts)
{
contentStream.AddCellContentLayout(hf, _dictionaries, _pageSettings);
contentStream.AddCellContentLayout(hf, _dictionaries, pageSettings);
}
foreach (var titleCell in printTitleLayouts)
{
Expand All@@ -214,7 +217,7 @@ private void AddContent(Transform pageLayout, PdfPage page)
case PdfCellLayout layout:
contentStream.AddCellLayout(layout, GetPatternLabel(layout)); break;
case PdfCellContentLayout contentLayout:
contentStream.AddCellContentLayout(contentLayout, _dictionaries, _pageSettings); break;
contentStream.AddCellContentLayout(contentLayout, _dictionaries, pageSettings); break;
case PdfCellBorderLayout borderLayout:
contentStream.AddBorderLayout(borderLayout); break;
}
Expand All@@ -232,16 +235,16 @@ private PdfInfoObject AddInfoObject(string workBookName = "")
return info;
}

internal void CreatePdf(PdfPageSettings pageSettings, PdfDictionaries dictionaries, Transform layout, string fileName)
internal void CreatePdf(PdfDocumentSettings documentSettings, PdfDictionaries dictionaries, Transform layout, string fileName)
{
//Write the PDF to the file. The Stream overload does the actual work and
//populates _debugString.
using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write))
{
CreatePdf(pageSettings, dictionaries, layout, fs);
CreatePdf(documentSettings, dictionaries, layout, fs);
}
//Write pdf as txt for debug.
if (_pageSettings.Debug && _pageSettings.PrintAsText)
if (_documentSettings.Debug && _documentSettings.PrintAsText)
{
using (var fs = new FileStream(fileName + ".txt", FileMode.Create, FileAccess.Write))
{
Expand All@@ -253,15 +256,16 @@ internal void CreatePdf(PdfPageSettings pageSettings, PdfDictionaries dictionari
}
}

internal void CreatePdf(PdfPageSettings pageSettings, PdfDictionaries dictionaries, Transform layout, Stream stream)
internal void CreatePdf(PdfDocumentSettings documentSettings, PdfDictionaries dictionaries, Transform layout, Stream stream)
{
if (stream == null) throw new ArgumentNullException(nameof(stream));
if (!stream.CanWrite) throw new ArgumentException("The stream must be writable.", nameof(stream));
//The cross-reference table stores byte offsets that the PDF reader uses to
//seek to each object, so the target stream has to support querying its position.
if (!stream.CanSeek) throw new ArgumentException("The stream must be seekable, because the PDF cross-reference table requires byte offsets.", nameof(stream));

_pageSettings = pageSettings;
//_pageSettings = pageSettings;
_documentSettings = documentSettings;
_dictionaries = dictionaries;
var catalog = AddCatalog(2);
//Create Pages
Expand All@@ -276,8 +280,8 @@ internal void CreatePdf(PdfPageSettings pageSettings, PdfDictionaries dictionari
//Create Page and Content
for (int i = 0; i < layout.ChildObjects.Count; i++)
{
var pageLayout = layout.ChildObjects[i];
var page = AddPage(2, new List<int>(), _pageSettings);
var pageLayout = (PdfPageLayout)layout.ChildObjects[i];
var page = AddPage(2, new List<int>(), pageLayout.Settings);
AddContent(pageLayout, page);
pages.pageObjectNumbers.Add(page.objectNumber);
}
Expand Down
7 changes: 6 additions & 1 deletion src/EPPlus.Export.Pdf/Layout/PdfPageLayout.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
using EPPlus.Export.Pdf.Settings;
using EPPlus.Graphics;
using System.Collections.Generic;
using System.Diagnostics;
Expand All@@ -27,7 +28,11 @@ internal class PdfPageLayout : Transform
public double PrintTitleWidth;
public double PrintTitleHeight;
public bool isCommentsPage = false;

/// <summary>
/// The settings of the worksheet this page belongs to.
/// Set in PdfLayout.GetCatalog, read in ExcelPdf when writing the page.
/// </summary>
internal PdfPageSettings Settings;
public PdfPageLayout(double x, double y, double width, double height)
: base(x, y, width, height)
{ }
Expand Down
37 changes: 37 additions & 0 deletions src/EPPlus.Export.Pdf/Settings/PdfDocumentSettings.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
using EPPlus.Fonts.OpenType;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EPPlus.Export.Pdf.Settings
{
internal class PdfDocumentSettings
{
internal OpenTypeFontEngine FontEngine;
internal List<string> FontDirectories;
internal bool SearchSystemDirectories;
internal bool EmbeddFonts;
internal string defaultFontName;
internal int FirstPageNumber;
internal bool Debug;
internal bool PrintAsText;

internal static PdfDocumentSettings From(PdfPageSettings s)
{
return new PdfDocumentSettings
{
FontEngine = s.FontEngine,
FontDirectories = s.FontDirectories,
SearchSystemDirectories = s.SearchSystemDirectories,
EmbeddFonts = s.EmbeddFonts,
defaultFontName = s.defaultFontName,
FirstPageNumber = s.FirstPageNumber,
Debug = s.Debug,
PrintAsText = s.PrintAsText,
};
}

}
}
14 changes: 13 additions & 1 deletion src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,7 +147,7 @@ public PdfPageSize PageSize
private Orientations _orientation = Orientations.Portrait;
/// <summary>
/// Set the orientation of the pages.
/// </summary>
/// </summary>
public Orientations Orientation
{
get
Expand DownExpand Up@@ -216,6 +216,18 @@ private static PdfPageSize ApplyOrientation(PdfPageSize size, Orientations orien
? size
: new PdfPageSize(size.Height, size.Width); // swap (ctor is width, height)
}

internal PdfPageSettings CloneForSheet()
{
var c = new PdfPageSettings(_fontEngine);
c.FontDirectories = FontDirectories;
c.SearchSystemDirectories = SearchSystemDirectories;
c.EmbeddFonts = EmbeddFonts;
c.defaultFontName = defaultFontName;
c.Debug = Debug;
c.PrintAsText = PrintAsText;
return c;
}
}

/// <summary>
Expand Down
10 changes: 8 additions & 2 deletions src/EPPlus/Export/PdfExport/Data/PageData.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,12 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
using EPPlus.Export.Pdf.Layout;
using EPPlus.Export.Pdf.Settings;
using OfficeOpenXml.Export.PdfExport.Layout;
using OfficeOpenXml.Export.PdfExport.TextMapping;
using OfficeOpenXml.Style;
using System.Collections.Generic;
using EPPlus.Export.Pdf.Layout;
using OfficeOpenXml.Export.PdfExport.Layout;


namespace OfficeOpenXml.Export.PdfExport.Data
Expand DownExpand Up@@ -50,6 +51,11 @@ internal struct Pages
public string HeadingFontName;
public float HeadingFontSize;
public ExcelFill HeadingFill;
/// <summary>
/// The settings of the worksheet these pages belong to.
/// Set in PdfLayout.GetPages, read in PdfLayout.GetCatalog.
/// </summary>
public PdfPageSettings Settings;
public int Count
{
get { return Width * Height; }
Expand Down
Loading
Loading