Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
27f8f52
#2477 - Fonts: Configurable font embedding policy (fsType / OnFontEmb…
swmal Aug 19, 2026
cc48b5d
Skip embedding decision now falls back to font chain (#2473)
swmal Aug 19, 2026
9a67e5b
WIP
swmal Aug 20, 2026
5804d68
Move font subsetting to document-wide DocumentFontSubsetBuilder
swmal Aug 24, 2026
501a2df
Merged from develop9
swmal Aug 24, 2026
0a86142
Fixed some merge conflicts
swmal Aug 25, 2026
6a9ef16
WIP
karlkallman Aug 25, 2026
d1a4386
Fix font nameID pairing so Arial Black resolves correctly (#2476)
Aug 26, 2026
5031a1f
WIP
karlkallman Aug 26, 2026
69ad648
Keep bundled fallback fonts embeddable when OnFontEmbedding skips
Aug 26, 2026
7061a74
WIP
karlkallman Aug 27, 2026
12a6fb4
WIP
karlkallman Aug 27, 2026
7e9dd12
Added functionality for centered content horiziontally and vertically…
karlkallman Aug 27, 2026
9c63117
Fix for bug #2485
karlkallman Aug 27, 2026
eb88e31
Fixed header footer issue & performance for table style.
AdrianEPPlus Aug 27, 2026
9e6a5e4
Added simple support for distibuter, justify and centered continious …
AdrianEPPlus Aug 27, 2026
51db119
if row height has not bee explicilty set, we do autofit on that row w…
AdrianEPPlus Aug 28, 2026
7412c9a
fixed comments and notes not having header and footer.
AdrianEPPlus Aug 28, 2026
6061b90
Added test
karlkallman Aug 28, 2026
4a54ce8
Changed radial gradients to box gradients.
AdrianEPPlus Aug 28, 2026
4428c35
double border fix progress
AdrianEPPlus Aug 31, 2026
2a0bf9e
double border progress
AdrianEPPlus Aug 31, 2026
6c04f61
progress
AdrianEPPlus Aug 31, 2026
1d5af53
WIP
karlkallman Aug 31, 2026
f4d3c21
WIP
karlkallman Sep 1, 2026
c8cafbc
Fix #2845: infinite loop and off-page content for oversized columns
karlkallman Sep 1, 2026
c3a63bc
double borders done for now.
AdrianEPPlus Sep 1, 2026
cf0e70b
Merge branch 'feature/i2476' into feature/pdf-to-develop9
AdrianEPPlus Sep 1, 2026
b0fc806
Merge branch 'feature/PdfCenterContentOnPage' of https://github.com/e…
AdrianEPPlus Sep 1, 2026
39fc47f
fixed missing fill, content and borders in merged cells.
AdrianEPPlus Sep 2, 2026
ea48e78
Merge branch 'bug/i2485' into feature/pdf-to-develop9
AdrianEPPlus Sep 2, 2026
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
37 changes: 30 additions & 7 deletions src/EPPlus.Export.Pdf.Tests/FontTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This software is licensed under PolyForm Noncommercial License 1.0.0
using EPPlus.Export.Pdf.Resources;
using EPPlus.Export.Pdf.Settings;
using EPPlus.Fonts.OpenType;
using EPPlus.Fonts.OpenType.Integration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml.Interfaces.Fonts;
using System;
Expand Down Expand Up @@ -57,11 +58,33 @@ private static PdfPageSettings CreateSettings(OpenTypeFontEngine engine, bool em
return settings;
}

private static PdfDictionaries CreateDictionariesWithSingleFont(PdfPageSettings settings)
private static PdfDictionaries CreateDictionariesWithSingleFont(PdfPageSettings settings, OpenTypeFontEngine engine)
{
var dictionaries = new PdfDictionaries();
// Register one font with some text so a subset is produced.
dictionaries.AddFont(settings, TestFontName, FontSubFamily.Regular, "Hello world!");

// In the new model Fonts is populated during shaping (ShapeText creates the resource,
// GidsAndCharMap fills gids + charmap), NOT by AddFont. Reproduce that end state directly
// so AddFontData has a realistic embedded resource to emit, without running a full export.
var font = engine.LoadFont(TestFontName, FontSubFamily.Regular);
var key = new FontKey(font.GetEnglishFontFamilyName(), font.NameTable.GetSubfamilyEnum());

var resource = new PdfFontResource(font.GetEnglishFontFamilyName(), font.NameTable.GetSubfamilyEnum(), 1, settings);
resource.fontData = font;

// Populate a few glyphs as shaping would, so the embedded path (CIDSet, font stream subset)
// has real glyph ids to work with.
ushort gid;
foreach (var ch in "Hi")
{
if (font.CmapTable.TryGetGlyphId(ch, out gid) && gid != 0)
{
resource.Gids.Add(gid);
if (!resource.charactermappings.ContainsKey(gid))
resource.charactermappings[gid] = ch.ToString();
}
}

dictionaries.Fonts[key] = resource;
return dictionaries;
}

Expand All @@ -77,12 +100,12 @@ public void AddFontData_Embedded_FontResourcePointsAtType0Dict()
using (var engine = CreateEngine())
{
var settings = CreateSettings(engine, true);
var dictionaries = CreateDictionariesWithSingleFont(settings);
var dictionaries = CreateDictionariesWithSingleFont(settings, engine);

var excelPdf = new ExcelPdf();
excelPdf.SetPageSettingsForTest(settings);
excelPdf.SetDocumentSettingsForTest(PdfDocumentSettings.From(settings));
excelPdf.SetDictionariesForTest(dictionaries);

excelPdf.AddFontData();

var fontResource = dictionaries.GetFont(settings, TestFontName, FontSubFamily.Regular);
Expand Down Expand Up @@ -126,12 +149,12 @@ public void AddFontData_Embedded_DoesNotEmitSimpleFontObject()
using (var engine = CreateEngine())
{
var settings = CreateSettings(engine, true);
var dictionaries = CreateDictionariesWithSingleFont(settings);
var dictionaries = CreateDictionariesWithSingleFont(settings, engine);

var excelPdf = new ExcelPdf();
excelPdf.SetPageSettingsForTest(settings);
excelPdf.SetDocumentSettingsForTest(PdfDocumentSettings.From(settings));
excelPdf.SetDictionariesForTest(dictionaries);

excelPdf.AddFontData();

foreach (var obj in excelPdf._document)
Expand Down
23 changes: 23 additions & 0 deletions src/EPPlus.Export.Pdf.Tests/PdfTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,28 @@ protected void SaveAsPdf(ExcelWorksheet sheet, string pdfFileName)
var path = Path.Combine(_pdfPath, pdfFileName);
sheet.SaveAsPdf(path);
}

protected void SaveAsPdf(ExcelWorkbook wb, string pdfFileName)
{
if (!pdfFileName.ToLower().EndsWith(".pdf"))
{
pdfFileName += ".pdf";
}
var path = Path.Combine(_pdfPath, pdfFileName);
wb.SaveAsPdf(path);
}

protected void SaveAsPdf(ExcelWorkbook wb, string pdfFileName, params ExcelRangeBase[] ranges)
{
if (!pdfFileName.ToLower().EndsWith(".pdf"))
{
pdfFileName += ".pdf";
}
var path = Path.Combine(_pdfPath, pdfFileName);
if (ranges.Count() > 1)
wb.SaveAsPdf(path, ranges);
else
ranges[0].SaveAsPdf(path);
}
}
}
172 changes: 171 additions & 1 deletion src/EPPlus.Export.Pdf.Tests/PdfTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ Date Author Change
10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0
*************************************************************************************************/
using EPPlus.Export.Pdf.Settings;
using EPPlus.Export.Pdf.Tests;
using EPPlus.Export.Pdf.Settings.PdfPageSizes;
using EPPlus.Export.Pdf.Tests;
using OfficeOpenXml;
using OfficeOpenXml.Export.PdfExport;
using OfficeOpenXml.Interfaces.Fonts;
using OfficeOpenXml.Export.PdfExport.Data;
using OfficeOpenXml.Export.PdfExport.Layout;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
using OfficeOpenXml.Export.PdfExport.Settings;
using OfficeOpenXml.Style;
using System.Diagnostics;
Expand Down Expand Up @@ -506,6 +510,116 @@ public void SaveRangeToNonWritableStreamThrowsTest()
Assert.ThrowsExactly<ArgumentException>(() => range.SaveAsPdf(readOnly));
}

[TestMethod]
public void ThreeFonts_NoSkip_RendersAllThreeCorrectly()
{
// Baseline: three different fonts, no skipping. Verifies the normal path still works
// after the subsetting rewrite. Open the PDF and confirm A1/B1/C1 read correctly.
using var p = OpenPackage("ThreeFonts_NoSkip.xlsx", true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Style.Font.Name = "Aptos Narrow";
ws.Cells["A1"].Value = "A1";
ws.Cells["B1"].Style.Font.Name = "Times New Roman";
ws.Cells["B1"].Value = "B1";
ws.Cells["C1"].Style.Font.Name = "Arial";
ws.Cells["C1"].Value = "C1";

SaveAsPdf(ws, "ThreeFonts_NoSkip.pdf");
}

[TestMethod]
public void MultiSheetWorkbook()
{
// Baseline: three different fonts, no skipping. Verifies the normal path still works
// after the subsetting rewrite. Open the PDF and confirm A1/B1/C1 read correctly.
using var p = OpenPackage("MultiSheetWorkbook.xlsx", true);
p.Workbook.ConfigureFonts(x => x.SearchSystemDirectories = true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Value = "Sheet1:A1";

var ws2 = p.Workbook.Worksheets.Add("Sheet2");

ws2.Cells["A1"].Style.Font.Name = "Times New Roman";
ws2.Cells["A1"].Value = "Sheet2:A1";

SaveAsPdf(p.Workbook, "MultiSheetWorkbook.pdf");
}

[TestMethod]
public void MultiRanges()
{
// Baseline: three different fonts, no skipping. Verifies the normal path still works
// after the subsetting rewrite. Open the PDF and confirm A1/B1/C1 read correctly.
using var p = OpenPackage("MultiRanges.xlsx", true);
p.Workbook.ConfigureFonts(x => x.SearchSystemDirectories = true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Value = "Sheet1:A1";
ws.Cells["F100"].Value = "Sheet1:F100";

SaveAsPdf(p.Workbook, "MultiRanges.pdf", ws.Cells["A1"], ws.Cells["F100"]);
}

[TestMethod]
public void SingleRange()
{
// Baseline: three different fonts, no skipping. Verifies the normal path still works
// after the subsetting rewrite. Open the PDF and confirm A1/B1/C1 read correctly.
using var p = OpenPackage("SingleRange.xlsx", true);
p.Workbook.ConfigureFonts(x => x.SearchSystemDirectories = true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Value = "Sheet1:A1";

SaveAsPdf(p.Workbook, "SingleRange.pdf", ws.Cells["A1"]);
}

[TestMethod]
public void ArialBlack_RendersCorrectly()
{
// Baseline: three different fonts, no skipping. Verifies the normal path still works
// after the subsetting rewrite. Open the PDF and confirm A1/B1/C1 read correctly.
using var p = OpenPackage("ArialBlack.xlsx", true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Style.Font.Name = "Arial Black";
ws.Cells["A1"].Value = "A1";

SaveAsPdf(ws, "ArialBlack.pdf");
}

[TestMethod]
public void ThreeFonts_SkipAll_CollapseToSharedLastResort()
{
// The regression case: three fonts, all skipped via OnFontEmbedding. Expected AFTER the fix:
// - small PDF (one shared Archivo subset, not three whole fonts)
// - A1 / B1 / C1 render DISTINCTLY and correctly (not all "A1")
// - the PDF opens without corruption
using var p = OpenPackage("ThreeFonts_SkipAll.xlsx", true);
var ws = p.Workbook.Worksheets.Add("Sheet1");

ws.Cells["A1"].Style.Font.Name = "Aptos Narrow";
ws.Cells["A1"].Value = "A1";
ws.Cells["B1"].Style.Font.Name = "Times New Roman";
ws.Cells["B1"].Value = "B1";
ws.Cells["C1"].Style.Font.Name = "Arial";
ws.Cells["C1"].Value = "C1";

p.Workbook.ConfigureFonts(cfg =>
{
cfg.OnFontEmbedding(info =>
{
System.Diagnostics.Debug.WriteLine("OnFontEmbedding fired for: " + info.FontName);
return FontEmbeddingDecision.Skip;
});
});


SaveAsPdf(ws, "ThreeFonts_SkipAll.pdf");
}

[TestMethod]
// works as expected.
//[DataRow("PDFTest.xlsx", "C:\\epplustest\\pdf\\FullPageTest56.pdf", "Sheet1")]
Expand Down Expand Up @@ -761,5 +875,61 @@ public void EachWorksheetUsesItsOwnPaperSize()
}
}

[TestMethod]
public void HeaderFooterTest1()
{
using var p = OpenTemplatePackage("1.06-Salesreport.xlsx");
var ws = p.Workbook.Worksheets[0];
string path = _pdfPath + "HeaderFooterTest1.pdf";
ws.SaveAsPdf(path);
Assert.IsTrue(File.Exists(path), "PDF file was not created.");
AssertLooksLikePdf(File.ReadAllBytes(path));
}

[TestMethod]
public void GetOriginX_CenteringOff_ReturnsContentBoundsLeft()
{
var s = new PdfPageSettings(null);
var p = new Page()
{
FromRow = 1,
ToRow = 10,
FromColumn = 1,
ToColumn = 5,
UsedWidth = 100,
UsedHeight = 100,
RowHeights = new double[10]
};

Assert.AreEqual(s.ContentBounds.Left, PdfLayout.GetOriginX(s, p), 0.0001);
}

[TestMethod]
public void GetOrigin_FlagsAreIndependent()
{
var s = new PdfPageSettings(null);
s.CenterOnPageHorizontally = true;
var p = new Page()
{
FromRow = 1,
ToRow = 10,
FromColumn = 1,
ToColumn = 5,
UsedWidth = s.ContentBounds.Width - 100d,
UsedHeight = s.ContentBounds.Height - 200d,
RowHeights = new double[10]
};
Assert.AreEqual(s.ContentBounds.Left + 50d, PdfLayout.GetOriginX(s, p), 0.0001);
Assert.AreEqual(s.ContentBounds.Top, PdfLayout.GetOriginY(s, p), 0.0001);
}

[TestMethod]
public void GetClampedCellWidth_CellFitsWithinPage_ReturnsCellWidthUnchanged()
{
var s = new PdfPageSettings(null);

Assert.AreEqual(51.71d, PdfLayout.GetClampedCellWidth(s, 126.31d, 51.71d), 0.0001);
}

}
}
Loading
Loading