From 1863f5224e11fd1181b5764f6442624efef0064b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AdrianParn=C3=A9us?= Date: Tue, 15 Sep 2026 12:39:41 +0200 Subject: [PATCH] Fixed certain pngs not being transparent when they should be. --- src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 12 +- .../DocumentObjects/PdfImageXObject.cs | 14 ++- src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs | 105 ++++++++++++++++++ 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 4b78180d4..2d45bbfd0 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -18,14 +18,11 @@ Date Author Change using OfficeOpenXml.Export.PdfExport.Data; using OfficeOpenXml.Export.PdfExport.Layout; using OfficeOpenXml.Export.PdfExport.Settings; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Style; using OfficeOpenXml.Table; using System.Data; -using System.Diagnostics; using System.Globalization; -using System.Reflection.Metadata; using System.Text; using System.Text.RegularExpressions; @@ -1105,5 +1102,14 @@ private void InitDataTable() _dataTable.Rows.Add("Thailand", 69950850, 513120); _dataTable.Rows.Add("Vietnam", 98168833, 331212); } + + [TestMethod] + public void PngTransparentTest() + { + using var p = OpenTemplatePackage("Allsvenskan2001.xlsx"); + var ws = p.Workbook.Worksheets[0]; + string path = _pdfPath + "Allsvenskan2001.pdf"; + ws.SaveAsPdf(path); + } } } diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs index a0be8bcaa..71a728cd8 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/PdfImageXObject.cs @@ -101,6 +101,11 @@ public PdfImageXObject(int objectNumber, byte[] imageBytes, int version = 0) int hival = palette == null || palette.Length < 3 ? 0 : (palette.Length / 3) - 1; ColorSpace = "[ /Indexed /DeviceRGB " + hival + " <" + PngDecoder.ToHex(palette) + "> ]"; colors = 1; + if (PngDecoder.DecodeIndexedAlpha(imageBytes, width, height, bitDepth, out byte[] indexedAlpha)) + { + SoftMaskData = indexedAlpha; + HasSoftMask = true; + } break; default: ColorSpace = "/DeviceRGB"; @@ -217,8 +222,13 @@ internal static bool ProducesSoftMask(byte[] imageBytes) { if (!PngDecoder.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; + if (interlace != 0) + return false; + if ((colorType == 4 || colorType == 6) && bitDepth == 8) + return true; + if (colorType == 3) + return PngDecoder.TryReadChunk(imageBytes, "tRNS", out byte[] _); + return false; } if (GifDecoder.IsGif(imageBytes)) { diff --git a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs index ba2d5e297..c4efdc62c 100644 --- a/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs +++ b/src/EPPlus.Export.Pdf/Helpers/PngDecoder.cs @@ -137,6 +137,111 @@ internal static void DecodePngWithAlpha(byte[] pngBytes, int width, int height, deflatedAlpha = PdfFlate.CompressLeaveOpen(alpha); } + internal static bool TryReadChunk(byte[] d, string chunkType, out byte[] data) + { + data = null; + if (!IsPng(d)) return false; + 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; + if (type == chunkType) + { + data = new byte[len]; + System.Array.Copy(d, dataStart, data, 0, len); + return true; + } + if (type == "IEND") break; + p = dataStart + len + 4; + } + return false; + } + + internal static bool DecodeIndexedAlpha(byte[] pngBytes, int width, int height, int bitDepth, out byte[] deflatedAlpha) + { + deflatedAlpha = null; + if (!TryReadChunk(pngBytes, "tRNS", out byte[] trns) || trns == null || trns.Length == 0) + return false; + + byte[] filtered = PdfFlate.Decompress(ReadPngIdat(pngBytes, out byte[] _)); + const int bpp = 1; + int stride = (width * bitDepth + 7) / 8; + var prev = new byte[stride]; + var cur = new byte[stride]; + var alpha = new byte[width * height]; + int pos = 0, ai = 0; + for (int y = 0; y < height; y++) + { + int filter = pos < filtered.Length ? filtered[pos++] : 0; + for (int x = 0; x < stride; x++) + { + int raw = pos < filtered.Length ? filtered[pos++] : 0; + int a = x >= bpp ? cur[x - bpp] : 0; + int b = prev[x]; + int c = x >= bpp ? prev[x - bpp] : 0; + int val; + switch (filter) + { + case 1: //Sub + val = raw + a; + break; + case 2: //Up + val = raw + b; + break; + case 3: //Average + val = raw + ((a + b) >> 1); + break; + case 4: //Paeth + val = raw + Paeth(a, b, c); + break; + default: //None + val = raw; + break; + } + cur[x] = (byte)(val & 0xFF); + } + for (int x = 0; x < width; x++) + { + int index = GetSample(cur, x, bitDepth); + alpha[ai++] = index < trns.Length ? trns[index] : (byte)255; + } + var swap = prev; prev = cur; cur = swap; + } + deflatedAlpha = PdfFlate.CompressLeaveOpen(alpha); + return true; + } + + private static int GetSample(byte[] scanline, int x, int bitDepth) + { + switch (bitDepth) + { + case 8: + return scanline[x]; + case 4: + { + int b = scanline[x >> 1]; + return (x & 1) == 0 ? (b >> 4) & 0x0F : b & 0x0F; + } + case 2: + { + int b = scanline[x >> 2]; + int shift = 6 - 2 * (x & 3); + return (b >> shift) & 0x03; + } + case 1: + { + int b = scanline[x >> 3]; + int shift = 7 - (x & 7); + return (b >> shift) & 0x01; + } + default: + return scanline[x]; + } + } + internal static int Paeth(int a, int b, int c) { int p = a + b - c;