diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 002cafb66..57fde6388 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -671,6 +671,17 @@ public void TableDiff() wb.SaveAsPdf(path, ws0); } + [TestMethod] + public void PictureOutside() + { + using var p = OpenTemplatePackage("Pdf_picture_outside.xlsx"); + var wb = p.Workbook; + var ws0 = wb.Worksheets[0]; + ws0.PrinterSettings.ShowGridLines = true; + string path = _pdfPath + "PictureOutside.pdf"; + wb.SaveAsPdf(path, ws0); + } + [TestMethod] public void EPPlusToPdf() { @@ -931,5 +942,15 @@ public void GetClampedCellWidth_CellFitsWithinPage_ReturnsCellWidthUnchanged() Assert.AreEqual(51.71d, PdfLayout.GetClampedCellWidth(s, 126.31d, 51.71d), 0.0001); } + [TestMethod] + public void LargeTableTest1() + { + using var p = OpenTemplatePackage("BlazorSample1 (12).xlsx"); + var ws = p.Workbook.Worksheets[1]; + string path = _pdfPath + "LargeTableTest.pdf"; + ws.SaveAsPdf(path); + Assert.IsTrue(File.Exists(path), "PDF file was not created."); + AssertLooksLikePdf(File.ReadAllBytes(path)); + } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCIDFont.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCIDFont.cs index 26bc9cd17..f6f675cdf 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCIDFont.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCIDFont.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using EPPlus.Fonts.OpenType; using System; using System.Collections.Generic; @@ -63,10 +64,10 @@ internal override string RenderDictionary() $" /Subtype /{Subtype.ToString()}\n" + $" /BaseFont /{BaseFont}\n" + $" /CIDSystemInfo << /Registry ({CIDInfoObject.Registry}) /Ordering ({CIDInfoObject.Ordering}) /Supplement {CIDInfoObject.Supplement} >>\n" + - $" /FontDescriptor {FontDescriptorObjectNumber} 0 R"); + $" /FontDescriptor {FontDescriptorObjectNumber.ToPdfStringF0()} 0 R"); if (DW != null) { - sb.AppendFormat($"\n /DW {DW}"); + sb.AppendFormat($"\n /DW {DW.ToPdfStringF0()}"); } if (Gids != null) { @@ -99,10 +100,10 @@ internal override void RenderDictionary(BinaryWriter bw) $" /Subtype /{Subtype.ToString()}\n" + $" /BaseFont /{BaseFont}\n" + $" /CIDSystemInfo << /Registry ({CIDInfoObject.Registry}) /Ordering ({CIDInfoObject.Ordering}) /Supplement {CIDInfoObject.Supplement} >>\n" + - $" /FontDescriptor {FontDescriptorObjectNumber} 0 R"); + $" /FontDescriptor {FontDescriptorObjectNumber.ToPdfStringF0()} 0 R"); if (DW != null) { - sb.AppendFormat($"\n /DW {DW}"); + sb.AppendFormat($"\n /DW {DW.ToPdfStringF0()}"); } if (Gids != null) { @@ -146,7 +147,7 @@ private string BuildWidthsArray() i++; } sb.Append($"{startGid} ["); - sb.Append(string.Join(" ", widths.Select(w => w.ToString()).ToArray())); + sb.Append(string.Join(" ", widths.Select(w => w.ToPdfStringF0()).ToArray())); sb.Append("] "); } return sb.ToString(); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCidSet.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCidSet.cs index b67a8736b..cad13a41e 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCidSet.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfCidSet.cs @@ -26,7 +26,7 @@ public PdfCidSet(int objectNumber, byte[] cidSet , int version = 0) : base(objec internal override string RenderDictionary() { - return $"<< /Length {CidSet.Length} >>\n" + $"stream\n|BINARY DATA|\nendstream"; + return $"<< /Length {CidSet.Length.ToPdfStringF0()} >>\n" + $"stream\n|BINARY DATA|\nendstream"; } internal override void RenderDictionary(BinaryWriter bw) @@ -34,12 +34,12 @@ internal override void RenderDictionary(BinaryWriter bw) var body = PdfFlate.Compress(CidSet); if (body.Length < CidSet.Length) { - WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length} >>\nstream\n"); + WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(body); } else { - WriteAscii(bw, $"<< /Length {CidSet.Length} >>\nstream\n"); + WriteAscii(bw, $"<< /Length {CidSet.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(CidSet); } WriteAscii(bw, "\nendstream"); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFont.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFont.cs index 54ece281b..fccd9d078 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFont.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFont.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.IO; using System.Text; @@ -72,19 +73,19 @@ internal override string RenderDictionary() } if (firstChar > -1) { - sb.AppendFormat($" /FirstChar {firstChar}\n"); + sb.AppendFormat($" /FirstChar {firstChar.ToPdfStringF0()}\n"); } if(lastChar > -1) { - sb.AppendFormat($" /LastChar {lastChar}\n"); + sb.AppendFormat($" /LastChar {lastChar.ToPdfStringF0()}\n"); } if (widthObjectNumber > -1) { - sb.AppendFormat($" /Widths {widthObjectNumber } 0 R\n"); + sb.AppendFormat($" /Widths {widthObjectNumber.ToPdfStringF0()} 0 R\n"); } if (fontDescriptorObjectNumber > -1) { - sb.AppendFormat($" /FontDescriptor {fontDescriptorObjectNumber} 0 R\n"); + sb.AppendFormat($" /FontDescriptor {fontDescriptorObjectNumber.ToPdfStringF0()} 0 R\n"); } sb.AppendFormat($" /Encoding /{encoding} >>"); return sb.ToString(); @@ -108,19 +109,19 @@ internal override void RenderDictionary(BinaryWriter bw) } if (firstChar > -1) { - sb.AppendFormat($" /FirstChar {firstChar}\n"); + sb.AppendFormat($" /FirstChar {firstChar.ToPdfStringF0()}\n"); } if (lastChar > -1) { - sb.AppendFormat($" /LastChar {lastChar}\n"); + sb.AppendFormat($" /LastChar {lastChar.ToPdfStringF0()}\n"); } if (widthObjectNumber > -1) { - sb.AppendFormat($" /Widths {widthObjectNumber} 0 R\n"); + sb.AppendFormat($" /Widths {widthObjectNumber.ToPdfStringF0()} 0 R\n"); } if (fontDescriptorObjectNumber > -1) { - sb.AppendFormat($" /FontDescriptor {fontDescriptorObjectNumber} 0 R\n"); + sb.AppendFormat($" /FontDescriptor {fontDescriptorObjectNumber.ToPdfStringF0()} 0 R\n"); } sb.AppendFormat($" /Encoding /{encoding} >>"); WriteAscii(bw, sb.ToString()); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs index 811142379..d7a935b65 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs @@ -69,20 +69,20 @@ internal override string RenderDictionary() StringBuilder sb = new StringBuilder(); sb.AppendFormat($"<< /Type /FontDescriptor\n" + $" /FontName /{fontName.Replace(" ", "")}\n" + - $" /Flags {flags}\n" + + $" /Flags {flags.ToPdfStringF0()}\n" + $" /FontBBox [{fontBBox.X.ToPdfStringF0()} {fontBBox.Y.ToPdfStringF0()} {fontBBox.Width.ToPdfStringF0()} {fontBBox.Height.ToPdfStringF0()}]\n" + - $" /Ascent {ascent}\n" + - $" /Descent {descent}\n" + - $" /CapHeight {capheight}\n" + - $" /ItalicAngle {(int)italicAngle}\n" + - $" /StemV {(int)stemV}"); + $" /Ascent {ascent.ToPdfStringF0()}\n" + + $" /Descent {descent.ToPdfStringF0()}\n" + + $" /CapHeight {capheight.ToPdfStringF0()}\n" + + $" /ItalicAngle {((int)italicAngle).ToPdfStringF0()}\n" + + $" /StemV {((int)stemV).ToPdfStringF0()}"); if (CidSetObjectNumber > 0) { - sb.AppendFormat($"\n /CIDSet {CidSetObjectNumber} 0 R"); + sb.AppendFormat($"\n /CIDSet {CidSetObjectNumber.ToPdfStringF0()} 0 R"); } if (FontFile2ObjectNumber > 0) { - sb.AppendFormat($"\n /FontFile2 {FontFile2ObjectNumber} 0 R"); + sb.AppendFormat($"\n /FontFile2 {FontFile2ObjectNumber.ToPdfStringF0()} 0 R"); } sb.AppendFormat(" >>"); return sb.ToString(); @@ -93,7 +93,7 @@ internal override void RenderDictionary(BinaryWriter bw) StringBuilder sb = new StringBuilder(); sb.AppendFormat($"<< /Type /FontDescriptor\n" + $" /FontName /{fontName.Replace(" ", "")}\n" + - $" /Flags {flags}\n" + + $" /Flags {flags.ToPdfStringF0()}\n" + $" /FontBBox [{fontBBox.X.ToPdfStringF0()} {fontBBox.Y.ToPdfStringF0()} {fontBBox.Width.ToPdfStringF0()} {fontBBox.Height.ToPdfStringF0()}]\n" + $" /Ascent {ascent.ToPdfStringF0()}\n" + $" /Descent {descent.ToPdfStringF0()}\n" + @@ -102,7 +102,7 @@ internal override void RenderDictionary(BinaryWriter bw) $" /StemV {((int)stemV).ToPdfStringF0()}"); if (FontFile2ObjectNumber > 0) { - sb.AppendFormat($"\n /FontFile2 {FontFile2ObjectNumber} 0 R"); + sb.AppendFormat($"\n /FontFile2 {FontFile2ObjectNumber.ToPdfStringF0()} 0 R"); } sb.AppendFormat(" >>"); WriteAscii(bw, sb.ToString()); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontStream.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontStream.cs index 9c6cb4738..eca8cfc8b 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontStream.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontStream.cs @@ -30,14 +30,14 @@ internal override string RenderDictionary() { var fontBytes = FontData.Serialize(); var fontData = Encoding.ASCII.GetString(fontBytes); - return $"<< /Length {fontBytes.Length} /Length1 {fontBytes.Length} >>\n" + $"stream\n|BINARY DATA|\nendstream"; + return $"<< /Length {fontBytes.Length.ToPdfStringF0()} /Length1 {fontBytes.Length.ToPdfStringF0()} >>\n" + $"stream\n|BINARY DATA|\nendstream"; } internal override void RenderDictionary(BinaryWriter bw) { var fontBytes = FontData.Serialize(); var body = PdfFlate.Compress(fontBytes); - WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length} /Length1 {fontBytes.Length} >>\nstream\n"); + WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length.ToPdfStringF0()} /Length1 {fontBytes.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(body); WriteAscii(bw, "\nendstream"); } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfToUnicodeCMap.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfToUnicodeCMap.cs index c2caedea7..9196bbfe0 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfToUnicodeCMap.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfToUnicodeCMap.cs @@ -47,7 +47,7 @@ internal override string RenderDictionary() var cmapContent = GenerateCMapContent(); var length = Encoding.UTF8.GetByteCount(cmapContent); var sb = new StringBuilder(); - sb.AppendLine(string.Format("<< /Length {0} >>", length)); + sb.AppendLine(string.Format("<< /Length {0} >>", length.ToPdfStringF0())); sb.AppendLine("stream"); sb.Append(cmapContent); sb.Append("\nendstream"); @@ -59,7 +59,7 @@ internal override void RenderDictionary(BinaryWriter bw) var cmapContent = GenerateCMapContent(); var cmapBytes = Encoding.ASCII.GetBytes(cmapContent); var body = PdfFlate.Compress(cmapBytes); - WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length} >>\nstream\n"); + WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(body); WriteAscii(bw, "\nendstream"); } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfType0FontDict.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfType0FontDict.cs index 010a67346..ec6c1c9cb 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfType0FontDict.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfType0FontDict.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.IO; using System.Linq; using System.Text; @@ -40,10 +41,10 @@ internal override string RenderDictionary() $" /Subtype /Type0\n" + $" /BaseFont /{BaseFont}\n" + $" /Encoding /{Encoding}\n" + - $" /DescendantFonts [{DescendantFontsObjectNumbers} 0 R]"); + $" /DescendantFonts [{DescendantFontsObjectNumbers.ToPdfStringF0()} 0 R]"); if (ToUnicodeObjectNumber > 0) { - sb.AppendFormat($"\n /ToUnicode {ToUnicodeObjectNumber} 0 R"); + sb.AppendFormat($"\n /ToUnicode {ToUnicodeObjectNumber.ToPdfStringF0()} 0 R"); } sb.Append(" >>"); return sb.ToString(); @@ -57,10 +58,10 @@ internal override void RenderDictionary(BinaryWriter bw) $" /Subtype /Type0\n" + $" /BaseFont /{BaseFont}\n" + $" /Encoding /{Encoding}\n" + - $" /DescendantFonts [{DescendantFontsObjectNumbers} 0 R]"); + $" /DescendantFonts [{DescendantFontsObjectNumbers.ToPdfStringF0()} 0 R]"); if (ToUnicodeObjectNumber > 0) { - sb.AppendFormat($"\n /ToUnicode {ToUnicodeObjectNumber} 0 R"); + sb.AppendFormat($"\n /ToUnicode {ToUnicodeObjectNumber.ToPdfStringF0()} 0 R"); } sb.Append(" >>"); WriteAscii(bw, sb.ToString()); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Functions/PdfPostScriptCalculatorFunction.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Functions/PdfPostScriptCalculatorFunction.cs index 2bd487f7f..0d6da7046 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Functions/PdfPostScriptCalculatorFunction.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Functions/PdfPostScriptCalculatorFunction.cs @@ -104,7 +104,7 @@ internal override void RenderDictionary(BinaryWriter bw) { var bytes = Encoding.ASCII.GetBytes(_code); WriteAscii(bw, "<< /FunctionType 4 /Domain [ 0 1 0 1 ] /Range [ 0 1 0 1 0 1 ] " + - $"/Length {bytes.Length} >>\nstream\n"); + $"/Length {bytes.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(bytes); WriteAscii(bw, "\nendstream"); } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfShadingPattern.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfShadingPattern.cs index 8135ad90f..f412c30cf 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfShadingPattern.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfShadingPattern.cs @@ -34,7 +34,7 @@ internal override string RenderDictionary() var sb = new StringBuilder(); sb.AppendFormat($"<< /Type /Pattern\n" + $" /PatternType 2\n" + - $" /Shading {shadingObjectNumber} 0 R"); + $" /Shading {shadingObjectNumber.ToPdfStringF0()} 0 R"); if (Matrix != null) { var matrixStr = string.Join(" ", Matrix.Select(w => w.ToPdfStringF4()).ToArray()); @@ -49,7 +49,7 @@ internal override void RenderDictionary(BinaryWriter bw) var sb = new StringBuilder(); sb.AppendFormat($"<< /Type /Pattern\n" + $" /PatternType 2\n" + - $" /Shading {shadingObjectNumber} 0 R"); + $" /Shading {shadingObjectNumber.ToPdfStringF0()} 0 R"); if (Matrix != null) { var matrixStr = string.Join(" ", Matrix.Select(w => w.ToPdfStringF4()).ToArray()); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfTilingPattern.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfTilingPattern.cs index d5e47398d..296c744f1 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfTilingPattern.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Patterns/PdfTilingPattern.cs @@ -54,7 +54,7 @@ internal override string RenderDictionary() { var streamContent = fill.CreatePatternResource(); var bytes = Encoding.ASCII.GetBytes(streamContent); - sb.AppendFormat($"\n /Length {bytes.Length}"); + sb.AppendFormat($"\n /Length {bytes.Length.ToPdfStringF0()}"); sb.Append(" >>"); sb.AppendFormat($"\nstream\n{streamContent}\nendstream"); } @@ -86,7 +86,7 @@ internal override void RenderDictionary(BinaryWriter bw) { var streamContent = fill.CreatePatternResource(); var body = PdfFlate.Compress(Encoding.ASCII.GetBytes(streamContent)); - sb.AppendFormat($"\n /Filter /FlateDecode /Length {body.Length}"); + sb.AppendFormat($"\n /Filter /FlateDecode /Length {body.Length.ToPdfStringF0()}"); sb.Append(" >>"); WriteAscii(bw, sb.ToString()); WriteAscii(bw, "\nstream\n"); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfCatalog.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfCatalog.cs index cbb0f96f6..98ef1d062 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfCatalog.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfCatalog.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.IO; namespace EPPlus.Export.Pdf.DocumentObjects @@ -27,13 +28,13 @@ public PdfCatalog(int objectNumber, int pagesObjectNumber, int version = 0) internal override string RenderDictionary() { return $"<< /Type /Catalog\n" + - $" /Pages {pagesObjectNumber} 0 R >>"; + $" /Pages {pagesObjectNumber.ToPdfStringF0()} 0 R >>"; } internal override void RenderDictionary(BinaryWriter bw) { WriteAscii(bw, $"<< /Type /Catalog\n" + - $" /Pages {pagesObjectNumber} 0 R >>"); + $" /Pages {pagesObjectNumber.ToPdfStringF0()} 0 R >>"); } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs index bb39fd0d4..e1f6c1ebe 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfContentStream.cs @@ -312,6 +312,15 @@ public void AddInnerGridLines(Transform pageLayout) 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; @@ -398,14 +407,14 @@ internal override string RenderDictionary() { var content = string.Join("\n", commands.ToArray()) + "\n"; var bytes = Encoding.ASCII.GetBytes(content); - return $"<< /Length {bytes.Length} >>\n" + $"stream\n{content}endstream"; + return $"<< /Length {bytes.Length.ToPdfStringF0()} >>\n" + $"stream\n{content}endstream"; } internal override void RenderDictionary(BinaryWriter bw) { var content = string.Join("\n", commands.ToArray()) + "\n"; var body = PdfFlate.Compress(Encoding.ASCII.GetBytes(content)); - WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length} >>\nstream\n"); + WriteAscii(bw, $"<< /Filter /FlateDecode /Length {body.Length.ToPdfStringF0()} >>\nstream\n"); bw.Write(body); WriteAscii(bw, "\nendstream"); } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfCrossRefTable.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfCrossRefTable.cs index 57d7ddd8c..271467d58 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfCrossRefTable.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfCrossRefTable.cs @@ -10,7 +10,9 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; @@ -30,11 +32,11 @@ internal string WriteString(int bodyCount) { StringBuilder sb = new StringBuilder(); sb.Append("xref\n"); - sb.AppendFormat($"0 {bodyCount + 1}\n"); + sb.AppendFormat($"0 {(bodyCount + 1).ToPdfStringF0()}\n"); sb.Append("0000000000 65535 f \n"); foreach (long pos in positions) { - sb.AppendFormat(pos.ToString("D10") + " 00000 n \n"); + sb.AppendFormat(pos.ToString("D10", CultureInfo.InvariantCulture) + " 00000 n \n"); } return sb.ToString(); } @@ -43,11 +45,11 @@ internal void Write(BinaryWriter bw, long startPosition, int bodyCount) { StartPosition = startPosition; bw.Write(Encoding.ASCII.GetBytes("xref\n")); - bw.Write(Encoding.ASCII.GetBytes($"0 {bodyCount + 1}\n")); + bw.Write(Encoding.ASCII.GetBytes($"0 {(bodyCount + 1).ToPdfStringF0()}\n")); bw.Write(Encoding.ASCII.GetBytes("0000000000 65535 f \n")); foreach (long pos in positions) { - bw.Write(Encoding.ASCII.GetBytes(pos.ToString("D10") + " 00000 n \n")); + bw.Write(Encoding.ASCII.GetBytes(pos.ToString("D10", CultureInfo.InvariantCulture) + " 00000 n \n")); } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs new file mode 100644 index 000000000..a53030db0 --- /dev/null +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs @@ -0,0 +1,399 @@ +/************************************************************************************************* + 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.Helpers; +using OfficeOpenXml.Packaging.Ionic.Zlib; +using System.IO; +using System.Text; + +namespace EPPlus.Export.Pdf.DocumentObjects +{ + internal class PdfImageXObject : PdfObject + { + private readonly byte[] _bytes; + internal int Width { get; } + internal int Height { get; } + internal string ColorSpace { get; } + internal int BitsPerComponent { get; } + internal string Filter { get; } + internal string Decode { get; private set; } + internal string DecodeParms { get; } + + internal bool HasSoftMask { get; } + internal byte[] SoftMaskData { get; } + internal int SoftMaskObjectNumber { get; set; } + + public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) + : base(objectNumber, version) + { + if (IsJpeg(imageBytes)) + { + _bytes = imageBytes; + // A JPEG embeds verbatim: /DCTDecode is exactly the JPEG's own coding. + Filter = "DCTDecode"; + BitsPerComponent = 8; + ReadJpegInfo(imageBytes, out int width, out int height, out int components, out bool adobe); + Width = width; + Height = height; + if (components == 4) + { + ColorSpace = "/DeviceCMYK"; + // Adobe writes CMYK JPEGs with every channel inverted; flip them back so the + // picture doesn't render as a negative. Straight (non-Adobe) CMYK is left as-is. + if (adobe) Decode = "[ 1 0 1 0 1 0 1 0 ]"; + } + else + { + ColorSpace = components == 1 ? "/DeviceGray" : "/DeviceRGB"; + } + } + else if (IsPng(imageBytes)) + { + ReadPngHeader(imageBytes, out int width, out int height, out int bitDepth, out int colorType, out int _); + Width = width; + Height = height; + Filter = "FlateDecode"; + if (colorType == 6 || colorType == 4) + { + // Alpha channel present: decode the PNG and split colour from alpha. The colour + // samples become this image; the alpha rides along as a grayscale soft mask. + BitsPerComponent = 8; + ColorSpace = colorType == 6 ? "/DeviceRGB" : "/DeviceGray"; + DecodePngWithAlpha(imageBytes, width, height, colorType, out byte[] color, out byte[] alpha); + _bytes = color; // raw colour samples, re-deflated (no PNG predictor) + SoftMaskData = alpha; // raw alpha, re-deflated -> companion /SMask object + HasSoftMask = true; + } + else + { + // Opaque (0/2/3): keep the compressed pixel data as-is. The concatenated IDAT is a + // complete zlib stream of PNG-filtered rows — exactly what /FlateDecode + a PNG + // predictor expect — so the viewer does the inflate and un-filter for us. + BitsPerComponent = bitDepth; + _bytes = ReadPngIdat(imageBytes, out byte[] palette); + int colors; + switch (colorType) + { + case 0: // greyscale + ColorSpace = "/DeviceGray"; + colors = 1; + break; + case 3: // palette index -> RGB lookup table carried inline + int hival = palette == null || palette.Length < 3 ? 0 : (palette.Length / 3) - 1; + ColorSpace = "[ /Indexed /DeviceRGB " + hival + " <" + ToHex(palette) + "> ]"; + colors = 1; + break; + default: // colour type 2 (truecolour RGB) + ColorSpace = "/DeviceRGB"; + colors = 3; + break; + } + // Predictor 15 = "PNG optimum" (any of the five row filters), described by the + // pixel layout so the viewer can reverse the per-row filtering. + DecodeParms = "<< /Predictor 15 /Colors " + colors + + " /BitsPerComponent " + bitDepth + + " /Columns " + width + " >>"; + } + } + else + { + // Unsupported encodings are screened out in PrecomputeImages; keep a safe default so + // an unexpected byte stream can't crash the export (future formats add a branch above). + _bytes = imageBytes; + Filter = "DCTDecode"; + BitsPerComponent = 8; + ColorSpace = "/DeviceRGB"; + } + + } + + + private PdfImageXObject(int objectNumber, int version, byte[] deflatedGray, int width, int height) + : base(objectNumber, version) + { + _bytes = deflatedGray; + Width = width; + Height = height; + BitsPerComponent = 8; + ColorSpace = "/DeviceGray"; + Filter = "FlateDecode"; + } + + 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) + { + if (IsJpeg(imageBytes)) return true; + if (IsPng(imageBytes)) + { + if (!ReadPngHeader(imageBytes, out int _, out int _, out int bitDepth, out int colorType, out int interlace)) + return false; + if (interlace != 0) return false; // Adam7 not handled + if (colorType == 0 || colorType == 2 || colorType == 3) return true; // opaque, verbatim + if (colorType == 4 || colorType == 6) return bitDepth == 8; // alpha -> decode + soft mask + return false; + } + return false; + } + + internal static bool ProducesSoftMask(byte[] imageBytes) + { + if (!IsPng(imageBytes)) return false; + if (!ReadPngHeader(imageBytes, out int _, out int _, out int bitDepth, out int colorType, out int interlace)) + return false; + if (interlace != 0) return false; + return (colorType == 4 || colorType == 6) && bitDepth == 8; + } + + private static bool IsJpeg(byte[] d) => d != null && d.Length > 2 && d[0] == 0xFF && d[1] == 0xD8; + + private static readonly byte[] _pngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; + private static bool IsPng(byte[] d) + { + if (d == null || d.Length<_pngSignature.Length) return false; + for (int i = 0; i<_pngSignature.Length; i++) + if (d[i] != _pngSignature[i]) return false; + return true; + } + + private string DictHeader() + { + string smask = HasSoftMask ? $" /SMask {SoftMaskObjectNumber.ToPdfStringF0()} 0 R" : ""; + string decode = string.IsNullOrEmpty(Decode) ? "" : $" /Decode {Decode}"; + string decodeParms = string.IsNullOrEmpty(DecodeParms) ? "" : $" /DecodeParms {DecodeParms}"; + return "<< /Type /XObject /Subtype /Image" + + $" /Width {Width.ToPdfStringF0()} /Height {Height.ToPdfStringF0()}" + + $" /ColorSpace {ColorSpace} /BitsPerComponent {BitsPerComponent.ToPdfStringF0()}" + + smask + + decode + + $" /Filter /{Filter}" + decodeParms + + $" /Length {_bytes.Length.ToPdfStringF0()} >>"; + } + + internal override string RenderDictionary() + { + // Debug/text dump only — never the real output — so the binary body is elided. + return DictHeader() + $"\nstream\n<{_bytes.Length.ToPdfStringF0()} bytes of image data>\nendstream"; + } + + internal override void RenderDictionary(BinaryWriter bw) + { + WriteAscii(bw, DictHeader() + "\nstream\n"); + bw.Write(_bytes); // raw JPEG — not Flate-compressed (already DCT-coded) + WriteAscii(bw, "\nendstream"); + } + + // Minimal JPEG reader: walk the marker segments to the Start-Of-Frame and read the frame's + // height, width and component count. Handles baseline and progressive SOFs. + private static void ReadJpegInfo(byte[] d, out int width, out int height, out int components, out bool adobe) + { + width = 0; height = 0; components = 3; adobe = false; + if (d == null || d.Length < 4 || d[0] != 0xFF || d[1] != 0xD8) return; // not a JPEG + int i = 2; + while (i + 1 < d.Length) + { + if (d[i] != 0xFF) { i++; continue; } + byte marker = d[i + 1]; + if (marker == 0xFF) { i++; continue; } // fill byte + // Standalone markers without a length: SOI, EOI, RSTn, TEM. + if (marker == 0xD8 || marker == 0xD9 || (marker >= 0xD0 && marker <= 0xD7) || marker == 0x01) + { + i += 2; continue; + } + if (i + 3 >= d.Length) return; + int segLen = (d[i + 2] << 8) | d[i + 3]; + // Adobe APP14 marker (FF EE) with an "Adobe" payload: Adobe-written, so 4-channel + // data is stored inverted (the caller adds /Decode to correct it). APP14 precedes SOF. + if (marker == 0xEE && i + 8 < d.Length && + d[i + 4] == (byte)'A' && d[i + 5] == (byte)'d' && d[i + 6] == (byte)'o' && + d[i + 7] == (byte)'b' && d[i + 8] == (byte)'e') + { + adobe = true; + } + // SOF markers hold the frame size: C0..CF except C4 (DHT), C8 (JPG ext), CC (DAC). + if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) + { + if (i + 9 >= d.Length) return; + height = (d[i + 5] << 8) | d[i + 6]; + width = (d[i + 7] << 8) | d[i + 8]; + components = d[i + 9]; + return; + } + if (segLen < 2) return; // malformed + i += 2 + segLen; + } + } + + private static bool ReadPngHeader(byte[] d, out int width, out int height, out int bitDepth, out int colorType, out int interlace) + { + width = height = bitDepth = colorType = interlace = 0; + if (!IsPng(d)) return false; + int p = _pngSignature.Length; // first chunk starts after the signature + if (p + 8 + 13 > d.Length) return false; + if (Ascii(d, p + 4, 4) != "IHDR") return false; + int q = p + 8; // IHDR chunk data + width = ReadBE32(d, q); + height = ReadBE32(d, q + 4); + bitDepth = d[q + 8]; + colorType = d[q + 9]; + // q+10 compression, q+11 filter (both always 0), q+12 interlace (0 none, 1 Adam7). + interlace = d[q + 12]; + return true; + } + + // Walk the chunk list and return the concatenated IDAT data (the zlib pixel stream) plus the + // palette, if any. The zlib stream can be split across several IDAT chunks, so it is stitched + // back together in order. + private static byte[] ReadPngIdat(byte[] d, out byte[] palette) + { + palette = null; + using (var idat = new MemoryStream()) + { + int p = _pngSignature.Length; + while (p + 8 <= d.Length) + { + int len = ReadBE32(d, p); + string type = Ascii(d, p + 4, 4); + int dataStart = p + 8; + if (len < 0 || dataStart + len + 4 > d.Length) break; // truncated / malformed + if (type == "PLTE") + { + palette = new byte[len]; + System.Array.Copy(d, dataStart, palette, 0, len); + } + else if (type == "IDAT") + { + idat.Write(d, dataStart, len); + } + else if (type == "IEND") + { + break; + } + p = dataStart + len + 4; // skip data + 4-byte CRC + } + return idat.ToArray(); + } + } + + private static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, int colorType, + out byte[] deflatedColor, out byte[] deflatedAlpha) + { + int channels = colorType == 6 ? 4 : 2; // RGBA or grey+alpha + int colorChannels = colorType == 6 ? 3 : 1; + byte[] filtered = ZlibDecompress(ReadPngIdat(pngBytes, out byte[] _)); + + int stride = width * channels; // 8-bit: one byte per channel + var color = new byte[width * height * colorChannels]; + var alpha = new byte[width * height]; + var prev = new byte[stride]; + var cur = new byte[stride]; + int pos = 0, ci = 0, ai = 0; + for (int y = 0; y < height; y++) + { + int filter = pos < filtered.Length ? filtered[pos++] : 0; // per-row filter type byte + for (int x = 0; x < stride; x++) + { + int raw = pos < filtered.Length ? filtered[pos++] : 0; + int a = x >= channels ? cur[x - channels] : 0; // reconstructed byte to the left + int b = prev[x]; // byte above + int c = x >= channels ? prev[x - channels] : 0; // byte above-left + int val; + switch (filter) + { + case 1: val = raw + a; break; // Sub + case 2: val = raw + b; break; // Up + case 3: val = raw + ((a + b) >> 1); break; // Average + case 4: val = raw + Paeth(a, b, c); break; // Paeth + default: val = raw; break; // None + } + cur[x] = (byte)(val & 0xFF); + } + // De-interleave this row: colour bytes to the image, the last channel to the mask. + for (int x = 0; x < width; x++) + { + int p = x * channels; + if (colorType == 6) + { + color[ci++] = cur[p]; + color[ci++] = cur[p + 1]; + color[ci++] = cur[p + 2]; + alpha[ai++] = cur[p + 3]; + } + else + { + color[ci++] = cur[p]; + alpha[ai++] = cur[p + 1]; + } + } + var swap = prev; prev = cur; cur = swap; // this row becomes "previous" for the next + } + deflatedColor = ZlibCompress(color); + deflatedAlpha = ZlibCompress(alpha); + } + + // PNG Paeth predictor (integer, no Math dependency). + private static int Paeth(int a, int b, int c) + { + int p = a + b - c; + int pa = p > a ? p - a : a - p; + int pb = p > b ? p - b : b - p; + int pc = p > c ? p - c : c - p; + if (pa <= pb && pa <= pc) return a; + return pb <= pc ? b : c; + } + + // zlib (RFC 1950) round-trips: PNG IDAT and PDF /FlateDecode are both zlib streams, so the + // same codec decompresses the IDAT and compresses the split colour / alpha back. + private static byte[] ZlibDecompress(byte[] data) + { + using (var input = new MemoryStream(data)) + using (var z = new ZlibStream(input, CompressionMode.Decompress)) + using (var output = new MemoryStream()) + { + byte[] buffer = new byte[8192]; + int n; + while ((n = z.Read(buffer, 0, buffer.Length)) > 0) output.Write(buffer, 0, n); + return output.ToArray(); + } + } + + private static byte[] ZlibCompress(byte[] data) + { + using (var output = new MemoryStream()) + { + using (var z = new ZlibStream(output, CompressionMode.Compress, CompressionLevel.BestCompression, true)) + { + z.Write(data, 0, data.Length); + } // disposing flushes the final bytes + Adler-32 into output + return output.ToArray(); + } + } + + private static readonly char[] _hex = "0123456789ABCDEF".ToCharArray(); + private static string ToHex(byte[] bytes) + { + if (bytes == null) return ""; + var sb = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + { + sb.Append(_hex[b >> 4]); + sb.Append(_hex[b & 0x0F]); + } + return sb.ToString(); + } + + // Big-endian 32-bit read (PNG stores all integers most-significant byte first). + private static int ReadBE32(byte[] d, int i) => (d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]; + private static string Ascii(byte[] d, int i, int len) => Encoding.ASCII.GetString(d, i, len); + } +} diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfObject.cs index b8213d3c0..677e1f2d9 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfObject.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfObject.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.IO; using System.Text; @@ -29,7 +30,7 @@ public PdfObject(int objectNumber, int version = 0) public virtual string ToPdfString() { var sb = new StringBuilder(); - sb.AppendFormat("{0} {1} obj\n", objectNumber, version); + sb.AppendFormat("{0} {1} obj\n", objectNumber.ToPdfStringF0(), version.ToPdfStringF0()); sb.Append(RenderDictionary()); sb.Append("\nendobj\n"); return sb.ToString(); @@ -38,7 +39,7 @@ public virtual string ToPdfString() public virtual void ToPdfBytes(BinaryWriter bw) { var sb = new StringBuilder(); - WriteAscii(bw, $"{objectNumber} {version} obj\n"); + WriteAscii(bw, $"{objectNumber.ToPdfStringF0()} {version.ToPdfStringF0()} obj\n"); RenderDictionary(bw); WriteAscii(bw, $"\nendobj\n"); } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfPage.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfPage.cs index a7632d8c2..b225a254d 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfPage.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfPage.cs @@ -17,6 +17,7 @@ Date Author Change using System.IO; using System.Linq; using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Export.Pdf.DocumentObjects { @@ -26,6 +27,7 @@ internal class PdfPage : PdfObject internal readonly List contentObjectNumbers; PdfDictionaries dictionaries; internal PdfPageSize Size; + internal bool HasTransparency; public PdfPage(int objectNumber, int parentObjectNumber, List contentObjectNumbers, PdfPageSize size, PdfDictionaries dictionaries, int version = 0) : base(objectNumber, version) @@ -38,25 +40,29 @@ public PdfPage(int objectNumber, int parentObjectNumber, List contentObject internal override string RenderDictionary() { - var fontEntries = dictionaries.Fonts.Select(f => $"/{f.Value.Label} {f.Value.fontObjectNumber} 0 R").ToArray(); + var fontEntries = dictionaries.Fonts.Select(f => $"/{f.Value.Label} {f.Value.fontObjectNumber.ToPdfStringF0()} 0 R").ToArray(); var fonts = string.Join(" ", fontEntries); - var patternEntries = dictionaries.Patterns.Select(p => $"/{p.Value.Label} {p.Value.objectNumber} 0 R").ToArray(); + var patternEntries = dictionaries.Patterns.Select(p => $"/{p.Value.Label} {p.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); var patterns = string.Join(" ", patternEntries); - var shadingEntries = dictionaries.Shadings.Select(s => $"/{s.Value.Label} {s.Value.objectNumber} 0 R").ToArray(); + var shadingEntries = dictionaries.Shadings.Select(s => $"/{s.Value.Label} {s.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); var shadings = string.Join(" ", shadingEntries); - var contentEntries = contentObjectNumbers.Select(con => $"{con} 0 R").ToArray(); + var imageEntries = dictionaries.Images.Select(im => $"/{im.Value.Label} {im.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); + var images = string.Join(" ", imageEntries); + var contentEntries = contentObjectNumbers.Select(con => $"{con.ToPdfStringF0()} 0 R").ToArray(); StringBuilder sb = new StringBuilder(); sb.AppendFormat($"<< /Type /Page\n" + - $" /Parent {parentObjectNumber} 0 R\n"); + $" /Parent {parentObjectNumber.ToPdfStringF0()} 0 R\n"); bool hasFont = !string.IsNullOrEmpty(fonts); bool hasPattern = !string.IsNullOrEmpty(patterns); bool hasShading = !string.IsNullOrEmpty(shadings); + bool hasImage = !string.IsNullOrEmpty(images); if (hasFont || hasPattern || hasShading) { sb.AppendFormat($" /Resources <<\n"); if (hasFont ) sb.AppendFormat($" /Font << {fonts} >>\n"); if (hasPattern) sb.AppendFormat($" /Pattern << {patterns} >>\n"); if (hasShading) sb.AppendFormat($" /Shading << {shadings} >>\n"); + if (hasImage) sb.AppendFormat($" /XObject << {images} >>\n"); sb.AppendFormat($" >>\n"); } sb.AppendFormat($" /MediaBox [ 0 0 {Size.WidthPu.ToPdfString()} {Size.HeightPu.ToPdfString()} ]\n" + @@ -66,27 +72,33 @@ internal override string RenderDictionary() internal override void RenderDictionary(BinaryWriter bw) { - var fontEntries = dictionaries.Fonts.Select(f => $"/{f.Value.Label} {f.Value.fontObjectNumber} 0 R").ToArray(); + var fontEntries = dictionaries.Fonts.Select(f => $"/{f.Value.Label} {f.Value.fontObjectNumber.ToPdfStringF0()} 0 R").ToArray(); var fonts = string.Join(" ", fontEntries); - var patternEntries = dictionaries.Patterns.Select(p => $"/{p.Value.Label} {p.Value.objectNumber} 0 R").ToArray(); + var patternEntries = dictionaries.Patterns.Select(p => $"/{p.Value.Label} {p.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); var patterns = string.Join(" ", patternEntries); - var shadingEntries = dictionaries.Shadings.Select(s => $"/{s.Value.Label} {s.Value.objectNumber} 0 R").ToArray(); + var shadingEntries = dictionaries.Shadings.Select(s => $"/{s.Value.Label} {s.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); var shadings = string.Join(" ", shadingEntries); - var contentEntries = contentObjectNumbers.Select(con => $"{con} 0 R").ToArray(); + var imageEntries = dictionaries.Images.Select(im => $"/{im.Value.Label} {im.Value.objectNumber.ToPdfStringF0()} 0 R").ToArray(); + var images = string.Join(" ", imageEntries); + var contentEntries = contentObjectNumbers.Select(con => $"{con.ToPdfStringF0()} 0 R").ToArray(); StringBuilder sb = new StringBuilder(); sb.AppendFormat($"<< /Type /Page\n" + - $" /Parent {parentObjectNumber} 0 R\n"); + $" /Parent {parentObjectNumber.ToPdfStringF0()} 0 R\n"); bool hasFont = !string.IsNullOrEmpty(fonts); bool hasPattern = !string.IsNullOrEmpty(patterns); bool hasShading = !string.IsNullOrEmpty(shadings); + bool hasImage = !string.IsNullOrEmpty(images); if (hasFont || hasPattern || hasShading) { sb.AppendFormat($" /Resources <<\n"); if (hasFont ) sb.AppendFormat($" /Font << {fonts} >>\n"); if (hasPattern) sb.AppendFormat($" /Pattern << {patterns} >>\n"); if (hasShading) sb.AppendFormat($" /Shading << {shadings} >>\n"); + if (hasImage) sb.AppendFormat($" /XObject << {images} >>\n"); sb.AppendFormat($" >>\n"); } + if (HasTransparency) + sb.AppendFormat($" /Group << /Type /Group /S /Transparency /CS /DeviceRGB >>\n"); sb.AppendFormat($" /MediaBox [ 0 0 {Size.WidthPu.ToPdfString()} {Size.HeightPu.ToPdfString()} ]\n" + $" /Contents [ {string.Join(" ", contentEntries)} ] >>"); WriteAscii(bw, sb.ToString()); diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfPages.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfPages.cs index a2684d13f..d19f24841 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfPages.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfPages.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,19 +30,19 @@ public PdfPages(int objectNumber, List pageObjectNumbers, int version = 0) internal override string RenderDictionary() { - var kids = string.Join(" ", pageObjectNumbers.Select(n => $"{n} 0 R").ToArray()); + var kids = string.Join(" ", pageObjectNumbers.Select(n => $"{n.ToPdfStringF0()} 0 R").ToArray()); return $"<< /Type /Pages\n" + $" /Kids [ {kids} ]\n" + - $" /Count {pageObjectNumbers.Count} >>"; + $" /Count {pageObjectNumbers.Count.ToPdfStringF0()} >>"; } internal override void RenderDictionary(BinaryWriter bw) { - var kids = string.Join(" ", pageObjectNumbers.Select(n => $"{n} 0 R").ToArray()); + var kids = string.Join(" ", pageObjectNumbers.Select(n => $"{n.ToPdfStringF0()} 0 R").ToArray()); StringBuilder sb = new StringBuilder(); WriteAscii(bw, $"<< /Type /Pages\n" + $" /Kids [ {kids} ]\n" + - $" /Count {pageObjectNumbers.Count} >>"); + $" /Count {pageObjectNumbers.Count.ToPdfStringF0()} >>"); } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfTrailer.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfTrailer.cs index 80245bbf7..a79dc9102 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfTrailer.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfTrailer.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using System.IO; using System.Text; @@ -21,11 +22,11 @@ internal static string WriteString(int bodyCount, int catalogObjectNumber, int i { StringBuilder sb = new StringBuilder(); sb.AppendFormat("trailer\n"); - sb.AppendFormat($"<< /Size {bodyCount + 1}\n" + - $" /Root {catalogObjectNumber} 0 R\n" + - $" /Info {infoObjectNumber} 0 R >>\n"); + sb.AppendFormat($"<< /Size {(bodyCount + 1).ToPdfStringF0()}\n" + + $" /Root {catalogObjectNumber.ToPdfStringF0()} 0 R\n" + + $" /Info {infoObjectNumber.ToPdfStringF0()} 0 R >>\n"); sb.AppendFormat("startxref\n"); - sb.AppendFormat($"{crossRefStartPosition}\n"); + sb.AppendFormat($"{crossRefStartPosition.ToPdfStringF0()}\n"); sb.AppendFormat("%%EOF\n"); return sb.ToString(); } @@ -33,11 +34,11 @@ internal static string WriteString(int bodyCount, int catalogObjectNumber, int i internal static void Write(BinaryWriter bw, int bodyCount, int catalogObjectNumber, int infoObjectNumber, long crossRefStartPosition) { bw.Write(Encoding.ASCII.GetBytes($"trailer\n" + - $"<< /Size {bodyCount + 1}\n" + - $" /Root {catalogObjectNumber} 0 R\n" + - $" /Info {infoObjectNumber} 0 R >>\n" + + $"<< /Size {(bodyCount + 1).ToPdfStringF0()}\n" + + $" /Root {catalogObjectNumber.ToPdfStringF0()} 0 R\n" + + $" /Info {infoObjectNumber.ToPdfStringF0()} 0 R >>\n" + $"startxref\n" + - $"{crossRefStartPosition}\n" + + $"{crossRefStartPosition.ToPdfStringF0()}\n" + $"%%EOF\n")); } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Shadings/PdfFunctionBasedShading.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Shadings/PdfFunctionBasedShading.cs index b822c2612..7135995ba 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Shadings/PdfFunctionBasedShading.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Shadings/PdfFunctionBasedShading.cs @@ -43,7 +43,7 @@ private string Build() $" /ShadingType 1\n" + $" /ColorSpace /{ColorSpace.ToString()}\n" + $" /Domain [ {domainStr} ]\n" + - $" /Function {FunctionObjectNumber} 0 R >>"); + $" /Function {FunctionObjectNumber.ToPdfStringF0()} 0 R >>"); return sb.ToString(); } diff --git a/src/EPPlus.Export.Pdf/ExcelPdf.cs b/src/EPPlus.Export.Pdf/ExcelPdf.cs index 40d6cf001..7e587a50a 100644 --- a/src/EPPlus.Export.Pdf/ExcelPdf.cs +++ b/src/EPPlus.Export.Pdf/ExcelPdf.cs @@ -141,6 +141,27 @@ private void AddShadingsData() } } + //Add Images + private void AddImageData() + { + foreach (var image in _dictionaries.Images) + { + var img = image.Value.GetImageObject(_document.Count + 1); + if (img.HasSoftMask) + { + // Alpha PNG: the alpha channel is a separate grayscale /SMask object. Add it + // first, then point the image at it and shift the image (and the page /XObject + // reference in image.Value) to the next slot so all three numbers agree. + var mask = PdfImageXObject.CreateSoftMask(_document.Count + 1, img.SoftMaskData, img.Width, img.Height); + _document.Add(mask); + img.SoftMaskObjectNumber = mask.objectNumber; + img.objectNumber = _document.Count + 1; + image.Value.objectNumber = img.objectNumber; + } + _document.Add(img); + } + } + //Create Page private PdfPage AddPage(int pagesObjectNumber, List contentObjectNumbers, PdfPageSettings settings) { @@ -203,6 +224,13 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) contentStream.AddCommand($"% CELL BORDER : {border.Name}"); contentStream.AddBorderLayout(border); } + 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; + } //Close the clipping rectangle. contentStream.AddCommand("Q"); contentStream.AddCommand($"% Margin Clip End"); @@ -230,6 +258,13 @@ private void AddContent(PdfPageLayout pageLayout, PdfPage page) { contentStream.AddCellContentLayout(hf, _dictionaries, pageSettings); } + 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; + } foreach (var titleCell in printTitleLayouts) { contentStream.AddCommand($"% PRINT TITLE : {titleCell.Name}"); @@ -306,6 +341,7 @@ internal void CreatePdf(PdfDocumentSettings documentSettings, PdfDictionaries di AddContent(pageLayout, page); pages.pageObjectNumbers.Add(page.objectNumber); } + AddImageData(); var info = AddInfoObject(); _debugString = ""; //write to pdf diff --git a/src/EPPlus.Export.Pdf/Helpers/PdfString.cs b/src/EPPlus.Export.Pdf/Helpers/PdfString.cs index 6c49ff4d1..a49cac8e2 100644 --- a/src/EPPlus.Export.Pdf/Helpers/PdfString.cs +++ b/src/EPPlus.Export.Pdf/Helpers/PdfString.cs @@ -45,6 +45,7 @@ internal static string ToPdfStringF0(this double val) { return val.ToString("F0", CultureInfo.InvariantCulture); } + /// /// Returns the value formated for use in pdf document. /// @@ -54,5 +55,28 @@ internal static string ToPdfStringF0(this int val) { return val.ToString("F0", CultureInfo.InvariantCulture); } + + /// + /// Returns the value formated for use in pdf document. + /// + /// Value to turn into a string. + /// The value repsented as a string with no decimals. + internal static string ToPdfStringF0(this int? val) + { + int v = val ?? 0; + if (val == null) + return string.Empty; + return v.ToString("F0", CultureInfo.InvariantCulture); + } + + /// + /// Returns the value formated for use in pdf document. + /// + /// Value to turn into a string. + /// The value repsented as a string with no decimals. + internal static string ToPdfStringF0(this long val) + { + return val.ToString("F0", CultureInfo.InvariantCulture); + } } } diff --git a/src/EPPlus.Export.Pdf/Layout/ImageDrawInfo.cs b/src/EPPlus.Export.Pdf/Layout/ImageDrawInfo.cs new file mode 100644 index 000000000..f5344a6cf --- /dev/null +++ b/src/EPPlus.Export.Pdf/Layout/ImageDrawInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.Pdf.Layout +{ + internal struct ImageDrawInfo + { + public double X; + public double Y; + public double Width; + public double Height; + public byte[] ImageBytes; + public string Name; + } +} diff --git a/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs index 1765ee5c6..f3bb91d1d 100644 --- a/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs +++ b/src/EPPlus.Export.Pdf/Layout/PdfHeaderFooter.cs @@ -44,6 +44,12 @@ internal class PdfHeaderFooter public List NumberOfPagesIndexes = new List(); public List PageNumberIndexes = new List(); + public byte[] ImageBytes; + public double ImageWidth; + public double ImageHeight; + public int ImageFragmentIndex = -1; + public bool HasImage => ImageBytes != null; + public PdfHeaderFooter(List textFormats, List pageNumberIndexes, List numberOfPagesIndexes, HeaderFooterType type, HeaderFooterAlignment alignment, HeaderFooterSection section) { Content = new PdfCellBase(); @@ -55,4 +61,23 @@ public PdfHeaderFooter(List textFormats, List pageNumberIndex Section = section; } } + + internal class PdfHeaderFooterImage + { + public HeaderFooterType PageType; + public HeaderFooterAlignment Alignment; + public HeaderFooterSection Section; + public byte[] ImageBytes; + public double Width; + public double Height; + + public PdfHeaderFooterImage(byte[] imageBytes, double width, double height, HeaderFooterType type, HeaderFooterAlignment alignment, HeaderFooterSection section) + { + ImageBytes = imageBytes; + Width = width; + Height = height; + PageType = type; + Alignment = alignment; + Section = section; } + } } diff --git a/src/EPPlus.Export.Pdf/Layout/PdfImageLayout.cs b/src/EPPlus.Export.Pdf/Layout/PdfImageLayout.cs new file mode 100644 index 000000000..3fca93698 --- /dev/null +++ b/src/EPPlus.Export.Pdf/Layout/PdfImageLayout.cs @@ -0,0 +1,31 @@ +/************************************************************************************************* + 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 System.Diagnostics; + +namespace EPPlus.Export.Pdf.Layout +{ + [DebuggerDisplay("Image: {Name}")] + internal class PdfImageLayout : Transform + { + public byte[] ImageBytes; + + public bool IsHeaderFooter; + + public PdfImageLayout(double x, double y, double width, double height) + : base(x, y - height, width, height) + { + Z = 5; // paint above cell fills, text and borders + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Export.Pdf/Resources/PdfDictionaries.cs b/src/EPPlus.Export.Pdf/Resources/PdfDictionaries.cs index d8c40e83c..2f2765d96 100644 --- a/src/EPPlus.Export.Pdf/Resources/PdfDictionaries.cs +++ b/src/EPPlus.Export.Pdf/Resources/PdfDictionaries.cs @@ -27,6 +27,7 @@ internal class PdfDictionaries internal readonly Dictionary Fonts = new Dictionary(); internal readonly Dictionary Patterns = new Dictionary(); internal readonly Dictionary Shadings = new Dictionary(); + internal readonly Dictionary Images = new Dictionary(); internal Dictionary ShapedProviders = new Dictionary(); // One document-wide subset builder, replacing the per-font FontSubsetManager. Owns all @@ -125,5 +126,26 @@ internal PdfFontResource GetFont(PdfPageSettings pageSettings, string fontName, throw new KeyNotFoundException("Font: " + requestedKey + " is missing from dictionary."); } + + internal PdfImageResource AddImage(byte[] imageBytes) + { + var key = GetImageKey(imageBytes); + if (!Images.TryGetValue(key, out var res)) + { + int label = 1; + if (Images.Count > 0) label = Images.Last().Value.labelNumber + 1; + res = new PdfImageResource(label, imageBytes); + Images.Add(key, res); + } + return res; + } + + private static string GetImageKey(byte[] bytes) + { + using (var sha = System.Security.Cryptography.SHA1.Create()) + { + return System.Convert.ToBase64String(sha.ComputeHash(bytes)); + } + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs b/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs new file mode 100644 index 000000000..943f54661 --- /dev/null +++ b/src/EPPlus.Export.Pdf/Resources/PdfImageResource .cs @@ -0,0 +1,27 @@ +using EPPlus.Export.Pdf.DocumentObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.Pdf.Resources +{ + internal class PdfImageResource : PdfResource + { + internal int objectNumber; + internal readonly byte[] ImageBytes; + + public PdfImageResource(int labelNumber, byte[] imageBytes) + : base("Im", labelNumber) + { + ImageBytes = imageBytes; + } + + public PdfImageXObject GetImageObject(int objectNumber, int version = 0) + { + this.objectNumber = objectNumber; + return new PdfImageXObject(objectNumber, ImageBytes, version); + } + } +} diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index fe20194ed..76287e5a1 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -200,8 +200,8 @@ public PdfScaling Scaling internal string defaultFontName = ""; //DEBUG - internal bool Debug = true; - internal bool PrintAsText = true; + internal bool Debug = false; + internal bool PrintAsText = false; public PdfPageSettings(OpenTypeFontEngine fontEngine) { diff --git a/src/EPPlus/Export/PdfExport/Data/PageData.cs b/src/EPPlus/Export/PdfExport/Data/PageData.cs index fe2040ac6..005cdf4d4 100644 --- a/src/EPPlus/Export/PdfExport/Data/PageData.cs +++ b/src/EPPlus/Export/PdfExport/Data/PageData.cs @@ -32,6 +32,7 @@ internal struct Page public PdfCellCollection Map; public PdfHeaderFooterCollection HeaderFooters; public Dictionary MergedCells; + public List Images; public List PrintTitleCells; public List PrintTitleGridLines; public List PrintTitleHeadings; @@ -53,6 +54,7 @@ internal struct Pages public string HeadingFontName; public float HeadingFontSize; public ExcelFill HeadingFill; + /// /// The settings of the worksheet these pages belong to. /// Set in PdfLayout.GetPages, read in PdfLayout.GetCatalog. diff --git a/src/EPPlus/Export/PdfExport/Data/PdfDrawing.cs b/src/EPPlus/Export/PdfExport/Data/PdfDrawing.cs new file mode 100644 index 000000000..a4c0942ae --- /dev/null +++ b/src/EPPlus/Export/PdfExport/Data/PdfDrawing.cs @@ -0,0 +1,12 @@ +using OfficeOpenXml.Drawing; + +namespace OfficeOpenXml.Export.PdfExport.Data +{ + internal class PdfDrawing + { + public ExcelPicture Picture { get; } + public byte[] ImageBytes => Picture.Image.ImageBytes; // raw JPEG stream, embeds verbatim later + public ePictureType PictureType => Picture.Image.Type.Value; + public PdfDrawing(ExcelPicture picture) { Picture = picture; } + } +} diff --git a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs index 9261792ba..ebcfb216a 100644 --- a/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs +++ b/src/EPPlus/Export/PdfExport/Data/PdfWorksheet.cs @@ -22,7 +22,7 @@ namespace OfficeOpenXml.Export.PdfExport.Data internal class PdfWorksheet { public Dictionary CommentsAndNotesCollections = new Dictionary(); - + public List Drawings = new List(); public List Ranges = null; //Rename this public PdfRange CommentsAndNotes; public PdfHeaderFooterCollection HeaderFooters = null; diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index af5c01105..7e2a83585 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.DocumentObjects; using EPPlus.Export.Pdf.Helpers; using EPPlus.Export.Pdf.Layout; using EPPlus.Export.Pdf.Resources; @@ -17,6 +18,8 @@ Date Author Change using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; +using EPPlus.Graphics.Units; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Export.PdfExport.Data; using OfficeOpenXml.Export.PdfExport.TextShaping; using OfficeOpenXml.Interfaces.Fonts; @@ -219,6 +222,18 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio y -= rowHeight; x = contentStartX; //pageSettings.ContentBounds.Left; } + //AddImages(page, pageLayout, pdfPages[i].Drawings, contentStartX, contentStartY); + if (page.Images != null) + { + foreach (var img in page.Images) + { + pageLayout.AddChild(new PdfImageLayout(img.X, img.Y, img.Width, img.Height) + { + ImageBytes = img.ImageBytes, + Name = img.Name, + }); + } + } if (page.HeaderFooters != null) { //bool isVeryFirstPage = (i == 0 && j == 0); @@ -226,7 +241,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio //var leftH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Left); var hfType = page.HeaderFooters.GetPageType(physicalPageIndex); var leftH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Left); - if (leftH != null) + if (leftH != null && !leftH.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, leftH, displayedPageNumber, totalPages); var ascent = leftH.Content.TextLines[0].LargestAscent; @@ -239,7 +254,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } var centerH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Center); - if (centerH != null) + if (centerH != null && !centerH.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, centerH, displayedPageNumber, totalPages); var ascent = centerH.Content.TextLines[0].LargestAscent; @@ -253,7 +268,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } var rightH = page.HeaderFooters.Get(hfType, HeaderFooterSection.Header, HeaderFooterAlignment.Right); - if (rightH != null) + if (rightH != null && !rightH.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, rightH, displayedPageNumber, totalPages); var ascent = rightH.Content.TextLines[0].LargestAscent; @@ -266,7 +281,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } var leftF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Left); - if (leftF != null) + if (leftF != null && !leftF.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, leftF, displayedPageNumber, totalPages); int last = leftF.Content.TextLines.Count - 1; @@ -280,7 +295,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } var centerF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Center); - if (centerF != null) + if (centerF != null && !centerF.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, centerF, displayedPageNumber, totalPages); int last = centerF.Content.TextLines.Count - 1; @@ -294,7 +309,7 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.AddChild(text); } var rightF = page.HeaderFooters.Get(hfType, HeaderFooterSection.Footer, HeaderFooterAlignment.Right); - if (rightF != null) + if (rightF != null && !rightF.HasImage) { SubstitutePageNumbers(pageSettings, dictionaries, rightF, displayedPageNumber, totalPages); int last = rightF.Content.TextLines.Count - 1; @@ -307,6 +322,97 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio text.GidsAndCharMap(dictionaries); pageLayout.AddChild(text); } + double hfContentLeft = pageSettings.Margins.LeftPu; + double hfContentWidth = pageSettings.PageSize.WidthPu - pageSettings.Margins.LeftPu - pageSettings.Margins.RightPu; + foreach (var hfSection in new[] { HeaderFooterSection.Header, HeaderFooterSection.Footer }) + { + foreach (var hfAlign in new[] { HeaderFooterAlignment.Left, HeaderFooterAlignment.Center, HeaderFooterAlignment.Right }) + { + var hf = page.HeaderFooters.Get(hfType, hfSection, hfAlign); + if (hf == null || !hf.HasImage) continue; + + // Fill page-number placeholders before measuring/splitting; + // the recorded indexes are into the full fragment list. + ApplyPageNumbers(hf, displayedPageNumber, totalPages); + + // ── horizontal: split the run at the image ── + var frags = hf.Content.TextFragments; + int splitAt = hf.ImageFragmentIndex; + if (splitAt < 0 || splitAt > frags.Count) splitAt = frags.Count; + var beforeFrags = frags.GetRange(0, splitAt); + var afterFrags = frags.GetRange(splitAt, frags.Count - splitAt); + + double beforeWidth, beforeAscent, beforeDescent; + double afterWidth, afterAscent, afterDescent; + var beforeRun = ShapeHeaderFooterRun(pageSettings, dictionaries, beforeFrags, hfSection, out beforeWidth, out beforeAscent, out beforeDescent); + var afterRun = ShapeHeaderFooterRun(pageSettings, dictionaries, afterFrags, hfSection, out afterWidth, out afterAscent, out afterDescent); + + bool canEmbed = PdfImageXObject.CanEmbed(hf.ImageBytes); + double imgW = canEmbed ? hf.ImageWidth : 0d; + double imgH = canEmbed ? hf.ImageHeight : 0d; + + double runWidth = beforeWidth + imgW + afterWidth; + double runStart; + switch (hfAlign) + { + case HeaderFooterAlignment.Left: + runStart = hfContentLeft; + break; + case HeaderFooterAlignment.Right: + runStart = hfContentLeft + hfContentWidth - runWidth; + break; + default: // Center + runStart = hfContentLeft + (hfContentWidth - runWidth) / 2d; + break; + } + + // ── vertical: image anchored at the margin, text dropped to the image bottom ── + // Shared text metrics so the before-text and after-text sit on one line. + double ascent = System.Math.Max(beforeAscent, afterAscent); + double descent = System.Math.Max(beforeDescent, afterDescent); + + // The image is anchored at the outer margin edge and grows inward, so no margin ◄──── this whole + // space is lost: a header image's top sits on the header line, a footer image's ◄──── block is the + // bottom on the footer line. (This is also where an image-only section sits.) ◄──── latest change + double imgTop = hfSection == HeaderFooterSection.Header + ? pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu + : pageSettings.Margins.FooterPu + imgH; + double imgBottom = imgTop - imgH; + + // Text baseline: line the text's bottom up with the image's bottom, but never let + // the text rise past the normal top-of-band position (only matters when an image is + // shorter than the text). With no embeddable image (imgH == 0) this reduces to the + // normal header/footer text position. + double naturalBaseline = hfSection == HeaderFooterSection.Header + ? pageSettings.PageSize.HeightPu - pageSettings.Margins.HeaderPu - ascent + : pageSettings.Margins.FooterPu + descent; + double textY = System.Math.Min(naturalBaseline, imgBottom + descent); // ◄──── end of latest change + + // ── emit before-text, image, after-text left-to-right ── + double cursor = runStart; + if (beforeRun != null) + { + pageLayout.AddChild(PlaceHeaderFooterRun(pageSettings, dictionaries, beforeRun, cursor, textY)); + } + cursor += beforeWidth; + + if (canEmbed) + { + pageLayout.AddChild(new PdfImageLayout(cursor, imgTop, imgW, imgH) // ◄──── imgTop now the margin-anchored value above + { + ImageBytes = hf.ImageBytes, + IsHeaderFooter = true, + Name = $"{hfSection}{hfAlign}Image", + }); + } + cursor += imgW; + + if (afterRun != null) + { + pageLayout.AddChild(PlaceHeaderFooterRun(pageSettings, dictionaries, afterRun, cursor, textY)); + } + } + } } PdfGridlinesLayout.AddGridLines(pageSettings, pages[j], pageLayout, borderOnly: !pageSettings.ShowGridLines || pdfPages[i].IsCommentsPage); pageLayout.ChildObjects.Sort((a, b) => @@ -673,6 +779,159 @@ private static void SetBorderStyle(PdfCellStyle style, PdfCellBorderLayout borde (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)diagUpStyle, diagUpColor, (EPPlus.Export.Pdf.Enums.ExcelBorderStyle)diagDownStyle, diagDownColor); } + + //private static void AddImages(Page page, PdfPageLayout pageLayout, List drawings, double contentStartX, double contentStartY) + //{ + // if (drawings == null) return; + // foreach (var drawing in drawings) + // { + // if (drawing.PictureType != ePictureType.Jpg) continue; // JPEG first + // var pic = drawing.Picture; + // if (pic.From == null) continue; // only cell-anchored pictures for now + // int imgRow = pic.From.Row + 1; // From.Row / From.Column are 0-based + // int imgCol = pic.From.Column + 1; + // if (imgRow < page.FromRow || imgRow > page.ToRow) continue; + // if (imgCol < page.FromColumn || imgCol > page.ToColumn) continue; + + // // Left edge: content origin + full widths of the columns before the anchor + EMU offset. + // double x = contentStartX; + // for (int c = page.FromColumn; c < imgCol; c++) + // x += page.Map[page.FromRow, c]?.ColumnWidth ?? 0d; + // x += pic.From.ColumnOff / (double)ExcelDrawing.EMU_PER_POINT; + + // // Top edge (Y-up): content origin - full heights of the rows above the anchor - EMU offset. + // double y = contentStartY; + // for (int r = page.FromRow; r < imgRow; r++) + // y -= page.RowHeights[r - page.FromRow]; + // y -= pic.From.RowOff / (double)ExcelDrawing.EMU_PER_POINT; + + // // Size: rendered pixel size (already resolved from the anchor by EPPlus) → points. + // double width = pic.GetPixelWidth() * ExcelDrawing.EMU_PER_PIXEL / (double)ExcelDrawing.EMU_PER_POINT; + // double height = pic.GetPixelHeight() * ExcelDrawing.EMU_PER_PIXEL / (double)ExcelDrawing.EMU_PER_POINT; + + // var image = new PdfImageLayout(x, y, width, height) + // { + // ImageBytes = drawing.ImageBytes, + // Name = "Image_" + pic.Name, + // }; + // pageLayout.AddChild(image); + // } + //} + + internal static Pages PrecomputeImages(PdfPageSettings pageSettings, PdfRange range, Pages pdfPages, List drawings, double zeroCharWidth) + { + if (drawings == null || drawings.Count == 0) return pdfPages; + + var colPrefix = new double[range.ColWidths.Count + 1]; + for (int i = 0; i < range.ColWidths.Count; i++) + colPrefix[i + 1] = colPrefix[i] + range.ColWidths[i]; + var rowPrefix = new double[range.RowHeights.Count + 1]; + for (int i = 0; i < range.RowHeights.Count; i++) + rowPrefix[i + 1] = rowPrefix[i] + range.RowHeights[i].Height; + + // Point distance from cell A1 to the range's top-left, so an absolute-anchored picture + // (positioned in EMU from A1) can be shifted into the same range-local space the cell + // anchors use. Zero when the range starts at A1, which is the common case. Column widths + // use the exporter's basis (ZeroCharWidth); row heights are already points. + var ws = range.Range.Worksheet; + double rangeOriginX = 0d; + for (int c = 1; c < range.Range._fromCol; c++) + rangeOriginX += ws.Column(c).Hidden ? 0d : UnitConversion.ExcelColumnWidthToPoints(ws.Column(c).Width, zeroCharWidth); + double rangeOriginY = 0d; + for (int r = 1; r < range.Range._fromRow; r++) + rangeOriginY += ws.Row(r).Hidden ? 0d : ws.Row(r).Height; + + for (int i = 0; i < pdfPages.Page.Length; i++) + pdfPages.Page[i] = PrecomputePageImages(pageSettings, range, pdfPages.Page[i], drawings, colPrefix, rowPrefix, rangeOriginX, rangeOriginY); + return pdfPages; + } + private static double PointsFromEmu(long emu) => emu / (double)ExcelDrawing.EMU_PER_POINT; + private static double PointsFromPixels(double pixels) => pixels * ExcelDrawing.EMU_PER_PIXEL / (double)ExcelDrawing.EMU_PER_POINT; + private static double ColumnEdge(double[] colPrefix, int localCol) => colPrefix[Math.Max(0, Math.Min(localCol, colPrefix.Length - 1))]; + private static double RowEdge(double[] rowPrefix, int localRow) => rowPrefix[Math.Max(0, Math.Min(localRow, rowPrefix.Length - 1))]; + private static Page PrecomputePageImages(PdfPageSettings pageSettings, PdfRange range, Page page, List drawings, double[] colPrefix, double[] rowPrefix, double rangeOriginX, double rangeOriginY) + { + page.Images = new List(); + int fromCol = range.Range._fromCol; + int fromRow = range.Range._fromRow; + + // This page's window in absolute (range-local) point space. + double pageAbsLeft = colPrefix[page.FromColumn - fromCol]; + double pageAbsRight = colPrefix[page.ToColumn - fromCol + 1]; + double pageAbsTop = rowPrefix[page.FromRow - fromRow]; + double pageAbsBottom = rowPrefix[page.ToRow - fromRow + 1]; + + double contentStartX = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; + double contentStartY = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + + foreach (var drawing in drawings) + { + if (!PdfImageXObject.CanEmbed(drawing.ImageBytes)) continue; + var pic = drawing.Picture; + double imgLeft, imgTop, imgRight, imgBottom; + if (pic.From != null) + { + // One-cell / two-cell: top-left is a cell + EMU offset. Resolve against the SAME + // column-width and row-height arrays the grid is drawn with, so the picture can't + // disagree with its columns (GetPixelWidth would use a different digit-width basis). + int imgColLocal = (pic.From.Column + 1) - fromCol; // From.Row/Column are 0-based + int imgRowLocal = (pic.From.Row + 1) - fromRow; + if (imgColLocal < 0 || imgColLocal >= colPrefix.Length - 1) continue; // anchor outside range + if (imgRowLocal < 0 || imgRowLocal >= rowPrefix.Length - 1) continue; + imgLeft = colPrefix[imgColLocal] + PointsFromEmu(pic.From.ColumnOff); + imgTop = rowPrefix[imgRowLocal] + PointsFromEmu(pic.From.RowOff); + if (pic.To != null) + { + // Two-cell: bottom-right is another cell + offset (same grid basis). + imgRight = ColumnEdge(colPrefix, (pic.To.Column + 1) - fromCol) + PointsFromEmu(pic.To.ColumnOff); + imgBottom = RowEdge(rowPrefix, (pic.To.Row + 1) - fromRow) + PointsFromEmu(pic.To.RowOff); + } + else + { + // One-cell: size is the intrinsic ext. GetPixelWidth/Height reduces to an exact + // EMU->point conversion here, so there is no digit-width basis to disagree with. + imgRight = imgLeft + PointsFromPixels(pic.GetPixelWidth()); + imgBottom = imgTop + PointsFromPixels(pic.GetPixelHeight()); + } + } + else if (pic.Position != null && pic.Size != null) + { + // Absolute anchor: fixed EMU position from cell A1 + fixed ext. Shift by the range + // origin so it lands in the same range-local space as the cell anchors. + imgLeft = PointsFromEmu(pic.Position.X) - rangeOriginX; + imgTop = PointsFromEmu(pic.Position.Y) - rangeOriginY; + imgRight = imgLeft + PointsFromEmu(pic.Size.Width); + imgBottom = imgTop + PointsFromEmu(pic.Size.Height); + } + else + { + continue; // no usable anchor (e.g. grouped / chart-relative) + } + double width = imgRight - imgLeft; + double height = imgBottom - imgTop; + + // Only place the picture on pages its rectangle actually overlaps (both axes). + if (imgLeft >= pageAbsRight || imgRight <= pageAbsLeft) continue; + if (imgTop >= pageAbsBottom || imgBottom <= pageAbsTop) continue; + + // Position relative to THIS page's content origin. When the picture starts on an + // earlier page the offset is negative; the page's margin clip trims the overflow. + double x = contentStartX + (imgLeft - pageAbsLeft); + double y = contentStartY - (imgTop - pageAbsTop); + + page.Images.Add(new ImageDrawInfo + { + X = x, + Y = y, + Width = width, + Height = height, + ImageBytes = drawing.ImageBytes, + Name = "Image_" + pic.Name, + }); + } + return page; + } + private static int GetTotalPages(List pdfPages) { int totalPages = 0; @@ -701,6 +960,7 @@ internal static List GetPages(PdfPageSettings[] sheetSettings, PdfWorkshe pages = PrecomputeMergedCells(pageSettings, range, pages); pages = PrecomputeSpillCells(pageSettings, range, pages); pages = PrecomputePrintTitleCells(pageSettings, pdfSheet, range, pages); + pages = PrecomputeImages(pageSettings, range, pages, pdfSheet.Drawings, pdfSheet.ZeroCharWidth); pages.HeadingFontName = pdfSheet.NormalStyle.Style.Font.Name; pages.HeadingFontSize = pdfSheet.NormalStyle.Style.Font.Size; pages.HeadingFill = pdfSheet.NormalStyle.Style.Fill; @@ -1495,22 +1755,70 @@ private static Pages GetHeaderFooter(PdfRange range, Pages pdfPages, PdfWorkshee return pdfPages; } + //private static void SubstitutePageNumbers(PdfPageSettings pageSettings, PdfDictionaries dictionaries, PdfHeaderFooter hf, int pageNumber, int totalPages) + //{ + // if (hf == null) return; + // if (hf.PageNumberIndexes.Count > 0) + // { + // foreach (var idx in hf.PageNumberIndexes) + // hf.Content.TextFragments[idx].Text = pageNumber.ToString(); + // } + // if (hf.NumberOfPagesIndexes.Count > 0) + // { + // foreach (var idx in hf.NumberOfPagesIndexes) + // hf.Content.TextFragments[idx].Text = totalPages.ToString(); + // } + // PdfTextShaper.ShapeText(pageSettings, dictionaries, hf.Content); + //} private static void SubstitutePageNumbers(PdfPageSettings pageSettings, PdfDictionaries dictionaries, PdfHeaderFooter hf, int pageNumber, int totalPages) { if (hf == null) return; - if (hf.PageNumberIndexes.Count > 0) - { - foreach (var idx in hf.PageNumberIndexes) - hf.Content.TextFragments[idx].Text = pageNumber.ToString(); - } - if (hf.NumberOfPagesIndexes.Count > 0) - { - foreach (var idx in hf.NumberOfPagesIndexes) - hf.Content.TextFragments[idx].Text = totalPages.ToString(); - } + ApplyPageNumbers(hf, pageNumber, totalPages); // ◄──── was two inline foreach loops PdfTextShaper.ShapeText(pageSettings, dictionaries, hf.Content); } + // Fills the page-number / number-of-pages placeholders in place, without shaping. Used both by the ◄──── new method + // plain-text path (which shapes afterwards) and the inline-image path (which shapes the split sub-runs). + private static void ApplyPageNumbers(PdfHeaderFooter hf, int pageNumber, int totalPages) + { + if (hf == null) return; + foreach (var idx in hf.PageNumberIndexes) + hf.Content.TextFragments[idx].Text = pageNumber.ToString(); + foreach (var idx in hf.NumberOfPagesIndexes) + hf.Content.TextFragments[idx].Text = totalPages.ToString(); + } + + private static PdfHeaderFooter ShapeHeaderFooterRun(PdfPageSettings pageSettings, PdfDictionaries dictionaries, + List frags, HeaderFooterSection section, + out double width, out double ascent, out double descent) + { + width = 0d; ascent = 0d; descent = 0d; + if (frags == null || frags.Count == 0) return null; + var sub = new PdfHeaderFooter(frags, new List(), new List(), HeaderFooterType.Odd, HeaderFooterAlignment.Left, section); + sub.Content.ContentAligmnet = new PdfCellAlignmentData + { + HorizontalAlignment = EPPlus.Export.Pdf.Enums.ExcelHorizontalAlignment.Left + }; + PdfTextShaper.ShapeText(pageSettings, dictionaries, sub.Content); + if (sub.Content.TextLines == null || sub.Content.TextLines.Count == 0 + || sub.Content.TextLines.LineFragments == null || sub.Content.TextLines.LineFragments.Count == 0 + || sub.Content.TotalTextLength <= 0d) + return null; + width = sub.Content.TextLines[0].Width; + ascent = sub.Content.TextLines[0].LargestAscent; + descent = sub.Content.TextLines[0].LargestDescent; + return sub; + } + + private static PdfCellContentLayout PlaceHeaderFooterRun(PdfPageSettings pageSettings, PdfDictionaries dictionaries, + PdfHeaderFooter run, double x, double baselineY) + { + var layout = new PdfCellContentLayout(pageSettings, dictionaries, run, x, baselineY, 0, 0); + layout.IsHeaderFooter = true; + layout.GidsAndCharMap(dictionaries); + return layout; + } + private static void AddIncomingSpill(Page page, PdfRange range, int fromRow, int toRow, int windowFromCol, int windowToCol, double windowOriginX, double windowOriginY, bool isPrintTitle) { if (page.SpillCells == null) return; // initialised by the caller diff --git a/src/EPPlus/Export/PdfExport/PdfCatalog.cs b/src/EPPlus/Export/PdfExport/PdfCatalog.cs index 4dab28018..fd3b96698 100644 --- a/src/EPPlus/Export/PdfExport/PdfCatalog.cs +++ b/src/EPPlus/Export/PdfExport/PdfCatalog.cs @@ -14,6 +14,7 @@ Date Author Change using EPPlus.Export.Pdf.Resources; using EPPlus.Export.Pdf.Settings; using EPPlus.Graphics; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Export.PdfExport.Data; using OfficeOpenXml.Export.PdfExport.Layout; using OfficeOpenXml.Export.PdfExport.RowResize; @@ -354,6 +355,7 @@ internal PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelWorkshe GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); + ReadDrawings(pdfSheet); return pdfSheet; } @@ -369,6 +371,7 @@ private PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelRangeBas GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); + ReadDrawings(pdfSheet); return pdfSheet; } @@ -416,6 +419,7 @@ private PdfWorksheet GetPdfWorksheet(PdfPageSettings pageSettings, ExcelWorkshee GetPrintTitles(pageSettings, pdfSheet); GetHeaderFooter(pageSettings, pdfSheet); GetCommentsAndNotes(pageSettings, pdfSheet); + ReadDrawings(pdfSheet); return pdfSheet; } @@ -433,7 +437,17 @@ private List GetRanges(ExcelWorksheet worksheet) else { var range = worksheet.DimensionByVisibility; - var pdfRange = new PdfRange(range, true); + int toRow = range?.End.Row ?? 1; + int toCol = range?.End.Column ?? 1; + foreach (var drawing in worksheet.Drawings) + { + drawing.GetToBounds(out int drawToRow, out _, out int drawToCol, out _); + if (drawToRow + 1 > toRow) toRow = drawToRow + 1; + if (drawToCol + 1 > toCol) toCol = drawToCol + 1; + } + if (toRow > ExcelPackage.MaxRows) toRow = ExcelPackage.MaxRows; + if (toCol > ExcelPackage.MaxColumns) toCol = ExcelPackage.MaxColumns; + var pdfRange = new PdfRange(worksheet.Cells[1, 1, toRow, toCol], true); pdfRange.ExtendColumns = true; ranges.Add(pdfRange); } @@ -542,5 +556,20 @@ private void GetCommentsAndNotes(PdfPageSettings pageSettings, PdfWorksheet pdfS pdfSheet.CommentsAndNotes = GetMaps(cnPageSettings, pdfSheet, pdfSheet.CommentsAndNotes); } } + + private void ReadDrawings(PdfWorksheet pdfSheet) + { + var worksheet = pdfSheet.Worksheet; + if (worksheet?.Drawings == null) return; + foreach (var drawing in worksheet.Drawings) + { + if (drawing is ExcelPicture picture) + { + var image = picture.Image; + if (image?.ImageBytes != null && image.Type.HasValue) + pdfSheet.Drawings.Add(new PdfDrawing(picture)); + } + } + } } } \ No newline at end of file diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs index 8ac65d2c0..1d345ccd8 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfHeaderFooterCollection.cs @@ -11,8 +11,9 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Export.Pdf.Layout; -using EPPlus.Export.Pdf.Settings; using EPPlus.Export.Pdf.Resources; +using EPPlus.Export.Pdf.Settings; +using OfficeOpenXml.Drawing.Vml; using OfficeOpenXml.Export.PdfExport.Data; using System.Collections.Generic; using System.Linq; diff --git a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs index 4d42233ac..171d051f4 100644 --- a/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs +++ b/src/EPPlus/Export/PdfExport/TextMapping/PdfTextMap.cs @@ -232,13 +232,21 @@ private static void GetFillStyles(ExcelRangeBase cell, PdfCellStyle cellStyle, D } else if (table.ShowRowStripes) { - var fill = (tableRow & 1) == 0 ? tableStyle.SecondRowStripe.Style.Fill : tableStyle.FirstRowStripe.Style.Fill; - if (fill.HasValue) cellStyle.dxfFill = fill; + var stripe = (tableRow & 1) == 0 + ? tableStyle.SecondRowStripe.Style.Fill + : tableStyle.FirstRowStripe.Style.Fill; + cellStyle.dxfFill = FillIsPaintable(stripe) + ? stripe + : tableStyle.WholeTable.Style.Fill; } else if (table.ShowColumnStripes) { - var fill = (tableCol & 1) != 0 ? tableStyle.SecondColumnStripe.Style.Fill : tableStyle.FirstColumnStripe.Style.Fill; - if (fill.HasValue) cellStyle.dxfFill = fill; + var stripe = (tableCol & 1) != 0 + ? tableStyle.SecondColumnStripe.Style.Fill + : tableStyle.FirstColumnStripe.Style.Fill; + cellStyle.dxfFill = FillIsPaintable(stripe) + ? stripe + : tableStyle.WholeTable.Style.Fill; } } } @@ -625,7 +633,7 @@ private static int MapUnderlineType(ExcelUnderLineType type) private static FontSubFamily ComputeFontStyle(TextFragment textFrag) { if (textFrag.RichTextOptions.Bold && textFrag.RichTextOptions.Italic) return FontSubFamily.BoldItalic; - if(textFrag.RichTextOptions.Bold) return FontSubFamily.Bold; + if (textFrag.RichTextOptions.Bold) return FontSubFamily.Bold; if (textFrag.RichTextOptions.Italic) return FontSubFamily.Italic; return FontSubFamily.Regular; } @@ -655,9 +663,24 @@ internal static PdfHeaderFooter GetTextFormats(PdfPageSettings pageSettings, Pdf var textFragments = new List(); List NumberOfPagesIndexes = new List(); List PageNumberIndexes = new List(); + byte[] imageBytes = null; + double imageWidth = 0, imageHeight = 0; + int imageFragmentIndex = -1; for (int i = 1; i < textCollection.Count; i++) { var hf = textCollection[i]; + if (hf.FormatCode == ExcelHeaderFooterFormattingCodes.Image) + { + var pic = textCollection.Picture; + if (pic?.Image?.ImageBytes != null) + { + imageBytes = pic.Image.ImageBytes; + imageWidth = pic.Width; + imageHeight = pic.Height; + imageFragmentIndex = textFragments.Count; + } + continue; + } var textFrag = new TextFragment(); textFrag.Font = new RichTextFormatSimple(); textFrag.Font.Family = string.IsNullOrEmpty(hf.FontName) ? ns.Style.Font.Name : hf.FontName; @@ -674,7 +697,7 @@ internal static PdfHeaderFooter GetTextFormats(PdfPageSettings pageSettings, Pdf textFrag.RichTextOptions.UnderlineType = hf.DoubleUnderline ? 4 : textFrag.RichTextOptions.UnderlineType; textFrag.RichTextOptions.StrikeType = hf.Striketrough ? 2 : 1; textFrag.RichTextOptions.FontColor = hf.Color; - textFrag.Font.SubFamily = ComputeFontStyle(textFrag); + textFrag.Font.SubFamily = ComputeFontStyle(textFrag); var text = string.Empty; switch (hf.FormatCode) { @@ -689,11 +712,11 @@ internal static PdfHeaderFooter GetTextFormats(PdfPageSettings pageSettings, Pdf break; case ExcelHeaderFooterFormattingCodes.NumberOfPages: text += "000"; - NumberOfPagesIndexes.Add(i-1); + NumberOfPagesIndexes.Add(i - 1); break; case ExcelHeaderFooterFormattingCodes.PageNumber: text += "000"; - PageNumberIndexes.Add(i-1); + PageNumberIndexes.Add(i - 1); break; case ExcelHeaderFooterFormattingCodes.CurrentTime: text += DateTime.Now.ToString("HH:mm"); @@ -710,7 +733,12 @@ internal static PdfHeaderFooter GetTextFormats(PdfPageSettings pageSettings, Pdf dictionaries.AddFont(pageSettings, textFrag.Font.Family, textFrag.Font.SubFamily, textFrag.Text); if (NumberOfPagesIndexes.Count > 0 || PageNumberIndexes.Count > 0) dictionaries.AddFont(pageSettings, textFrag.Font.Family, textFrag.Font.SubFamily, "1234567890"); } - return new PdfHeaderFooter(textFragments, PageNumberIndexes, NumberOfPagesIndexes, type, alignment, section); + var result = new PdfHeaderFooter(textFragments, PageNumberIndexes, NumberOfPagesIndexes, type, alignment, section); + result.ImageBytes = imageBytes; + result.ImageWidth = imageWidth; + result.ImageHeight = imageHeight; + result.ImageFragmentIndex = imageFragmentIndex; + return result; } /// @@ -1092,5 +1120,24 @@ private static ExcelTableNamedStyle GetTableStyle(ExcelTable table, Dictionary