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