From cd5db822ab87d678d7caba7ff91c5fe18933b656 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Mon, 20 Apr 2026 15:29:20 +0200 Subject: [PATCH 1/2] Change GrayScaleMethod from enum to delegate to allow users more control over separation images. --- .../Program.cs | 2 +- .../Content/GrayScaleMethod.cs | 79 +++++++++++++++++-- .../Content/PdfImage.cs | 36 ++------- .../Generation/PageResources.cs | 3 +- src/Synercoding.FileFormats.Pdf/PdfWriter.cs | 6 +- .../Generation/PageResourcesTests.cs | 2 +- 6 files changed, 86 insertions(+), 42 deletions(-) diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs index 7b0b755..4e627a6 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs @@ -294,7 +294,7 @@ private static void _writePdf(string fileName, bool enableSubsetting = true) using (var pantherSixImage = SixLabors.ImageSharp.Image.Load(pantherPngStream)) { var pantherImg = writer.AddImage(pantherSixImage); - var transparentPanther = writer.AddSeparationImage(pantherSixImage, new Separation(PdfName.Get("White"), PredefinedColors.Yellow), GrayScaleMethod.AlphaChannel, [0, 1]); + var transparentPanther = writer.AddSeparationImage(pantherSixImage, new Separation(PdfName.Get("White"), PredefinedColors.Yellow), GrayScaleMethods.AlphaChannel, [0, 1]); writer.AddPage(page => { diff --git a/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs b/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs index 0d9597f..176fd31 100644 --- a/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs +++ b/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs @@ -1,36 +1,99 @@ +using SixLabors.ImageSharp.PixelFormats; + namespace Synercoding.FileFormats.Pdf.Content; /// /// What method is used to generate a 1 component grayscale pixel byte array /// -public enum GrayScaleMethod +public static class GrayScaleMethods { /// /// Use the red channel /// - RedChannel, + public static byte RedChannel(ref Rgba32 pixel) => pixel.R; + /// + /// Use the red channel + /// + public static byte RedChannel(ref Rgb24 pixel) => pixel.R; + /// + /// Use the green channel + /// + public static byte GreenChannel(ref Rgba32 pixel) => pixel.G; /// /// Use the green channel /// - GreenChannel, + public static byte GreenChannel(ref Rgb24 pixel) => pixel.G; /// /// Use the blue channel /// - BlueChannel, + public static byte BlueChannel(ref Rgba32 pixel) => pixel.B; + /// + /// Use the blue channel + /// + public static byte BlueChannel(ref Rgb24 pixel) => pixel.B; /// /// Use the alpha channel /// - AlphaChannel, + public static byte AlphaChannel(ref Rgba32 pixel) => pixel.A; + /// + /// Use the average of the Red, Green and Blue channels. + /// + public static byte AverageOfRGBChannels(ref Rgba32 pixel) => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ); /// /// Use the average of the Red, Green and Blue channels. /// - AverageOfRGBChannels, + public static byte AverageOfRGBChannels(ref Rgb24 pixel) => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ); /// /// The constants defined by ITU-R BT.601 are 0.299 red + 0.587 green + 0.114 blue. /// - BT601, + public static byte BT601(ref Rgba32 pixel) => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ); + /// + /// The constants defined by ITU-R BT.601 are 0.299 red + 0.587 green + 0.114 blue. + /// + public static byte BT601(ref Rgb24 pixel) => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ); /// /// The constants defined by ITU-R BT.709 are 0.2126 red + 0.7152 green + 0.0722 blue. /// - BT709, + public static byte BT709(ref Rgba32 pixel) => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ); + /// + /// The constants defined by ITU-R BT.709 are 0.2126 red + 0.7152 green + 0.0722 blue. + /// + public static byte BT709(ref Rgb24 pixel) => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ); + + /// + /// Create a threshold method from an initial method. The threshold method will return the lowValue if the initial method returns a value less than the threshold, and the highValue otherwise. + /// + /// The initial method to use. + /// The threshold value. + /// The value to return if the initial method returns a value less than the threshold. + /// The value to return if the initial method returns a value greater than or equal to the threshold. + /// The threshold method. + public static GrayScaleMethod32 Threshold(GrayScaleMethod32 initialMethod, byte threshold, byte lowValue = 0x00, byte highValue = 0xFF) + { + return (ref Rgba32 pixel) => + { + var value = initialMethod(ref pixel); + return value < threshold ? lowValue : highValue; + }; + } + + /// + /// Create a threshold method from an initial method. The threshold method will return the lowValue if the initial method returns a value less than the threshold, and the highValue otherwise. + /// + /// The initial method to use. + /// The threshold value. + /// The value to return if the initial method returns a value less than the threshold. + /// The value to return if the initial method returns a value greater than or equal to the threshold. + /// The threshold method. + public static GrayScaleMethod24 Threshold(GrayScaleMethod24 initialMethod, byte threshold, byte lowValue = 0x00, byte highValue = 0xFF) + { + return (ref Rgb24 pixel) => + { + var value = initialMethod(ref pixel); + return value < threshold ? lowValue : highValue; + }; + } } + +public delegate byte GrayScaleMethod32(ref Rgba32 pixel); +public delegate byte GrayScaleMethod24(ref Rgb24 pixel); diff --git a/src/Synercoding.FileFormats.Pdf/Content/PdfImage.cs b/src/Synercoding.FileFormats.Pdf/Content/PdfImage.cs index e727991..6f704db 100644 --- a/src/Synercoding.FileFormats.Pdf/Content/PdfImage.cs +++ b/src/Synercoding.FileFormats.Pdf/Content/PdfImage.cs @@ -151,14 +151,14 @@ internal static PdfImage Get(TableBuilder tableBuilder, Image image) internal static PdfImage Get(TableBuilder tableBuilder, Image image) => new PdfImage(tableBuilder.ReserveId(), _encodeToJpg(image), image.Width, image.Height, DeviceRGB.Instance, null, null, (PdfNames.DCTDecode, null)); - internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image image, Separation separation, GrayScaleMethod grayScaleMethod, double[]? decodeArray = null) + internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image image, Separation separation, GrayScaleMethod24 grayScaleMethod, double[]? decodeArray = null) { using var grayScaleStream = AsGrayScaleByteStream(image, grayScaleMethod); return new PdfImage(tableBuilder.ReserveId(), _flateEncode(grayScaleStream), image.Width, image.Height, separation, null, decodeArray, (PdfNames.FlateDecode, null)); } - internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image image, Separation separation, GrayScaleMethod grayScaleMethod, double[]? decodeArray = null) + internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image image, Separation separation, GrayScaleMethod32 grayScaleMethod, double[]? decodeArray = null) { using var grayScaleStream = AsGrayScaleByteStream(image, grayScaleMethod); @@ -170,7 +170,7 @@ internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image if (!_hasTransparancy(image)) return null; - using var grayScaleStream = AsGrayScaleByteStream(image, GrayScaleMethod.AlphaChannel); + using var grayScaleStream = AsGrayScaleByteStream(image, GrayScaleMethods.AlphaChannel); return new PdfImage(tableBuilder.ReserveId(), _flateEncode(grayScaleStream), image.Width, image.Height, DeviceGray.Instance, null, null, (PdfNames.FlateDecode, null)); } @@ -214,7 +214,7 @@ private static Stream _flateEncode(MemoryStream stream) return new MemoryStream(bytes); } - internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScaleMethod grayScaleMethod) + internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScaleMethod32 grayScaleMethod) { var byteStream = new MemoryStream(); @@ -231,17 +231,7 @@ internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScal // Get a reference to the pixel at position x ref Rgba32 pixel = ref pixelRow[x]; - var pixelValue = grayScaleMethod switch - { - GrayScaleMethod.AlphaChannel => pixel.A, - GrayScaleMethod.RedChannel => pixel.R, - GrayScaleMethod.GreenChannel => pixel.G, - GrayScaleMethod.BlueChannel => pixel.B, - GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ), - GrayScaleMethod.BT601 => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ), - GrayScaleMethod.BT709 => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ), - _ => throw new NotImplementedException() - }; + var pixelValue = grayScaleMethod(ref pixel); byteStream.WriteByte(pixelValue); } @@ -253,11 +243,8 @@ internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScal return byteStream; } - internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScaleMethod grayScaleMethod) + internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScaleMethod24 grayScaleMethod) { - if (grayScaleMethod == GrayScaleMethod.AlphaChannel) - throw new ArgumentException($"Can not use alpha channel for images of pixel format {nameof(Rgb24)}.", nameof(grayScaleMethod)); - var byteStream = new MemoryStream(); image.ProcessPixelRows(accessor => @@ -273,16 +260,7 @@ internal static MemoryStream AsGrayScaleByteStream(Image image, GrayScale // Get a reference to the pixel at position x ref Rgb24 pixel = ref pixelRow[x]; - var pixelValue = grayScaleMethod switch - { - GrayScaleMethod.RedChannel => pixel.R, - GrayScaleMethod.GreenChannel => pixel.G, - GrayScaleMethod.BlueChannel => pixel.B, - GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ), - GrayScaleMethod.BT601 => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ), - GrayScaleMethod.BT709 => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ), - _ => throw new NotImplementedException() - }; + var pixelValue = grayScaleMethod(ref pixel); byteStream.WriteByte(pixelValue); } diff --git a/src/Synercoding.FileFormats.Pdf/Generation/PageResources.cs b/src/Synercoding.FileFormats.Pdf/Generation/PageResources.cs index 386b304..340d40e 100644 --- a/src/Synercoding.FileFormats.Pdf/Generation/PageResources.cs +++ b/src/Synercoding.FileFormats.Pdf/Generation/PageResources.cs @@ -69,8 +69,9 @@ public PdfName AddJpgUnsafe(Stream jpgStream, int originalWidth, int originalHei return Add(pdfImage); } - public PdfName Add(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels) + public PdfName Add(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod32? grayScaleMethod = null) { + grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels; var pdfImage = PdfImage.GetSeparation(_tableBuilder, image, separation, grayScaleMethod); return Add(pdfImage); diff --git a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs index fe43230..0c5312a 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs @@ -169,8 +169,9 @@ public PdfImage AddImage(SixLabors.ImageSharp.Image image) /// The method to convert to grayscale. /// Optional decode array for the image. /// The added PDF image. - public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels, double[]? decodeArray = null) + public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod32? grayScaleMethod = null, double[]? decodeArray = null) { + grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels; var pdfImage = PdfImage.GetSeparation(_objectWriter.TableBuiler, image, separation, grayScaleMethod, decodeArray); _objectWriter.Write(pdfImage.ToStreamObject(_cachedResources)); @@ -193,8 +194,9 @@ public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image image, Sep /// The method to convert to grayscale. /// Optional decode array for the image. /// The added PDF image. - public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels, double[]? decodeArray = null) + public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image image, Separation separation, GrayScaleMethod24? grayScaleMethod = null, double[]? decodeArray = null) { + grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels; var pdfImage = PdfImage.GetSeparation(_objectWriter.TableBuiler, image, separation, grayScaleMethod, decodeArray); _objectWriter.Write(pdfImage.ToStreamObject(_cachedResources)); diff --git a/tests/Synercoding.FileFormats.Pdf.Tests/Generation/PageResourcesTests.cs b/tests/Synercoding.FileFormats.Pdf.Tests/Generation/PageResourcesTests.cs index 7af998f..4728e6d 100644 --- a/tests/Synercoding.FileFormats.Pdf.Tests/Generation/PageResourcesTests.cs +++ b/tests/Synercoding.FileFormats.Pdf.Tests/Generation/PageResourcesTests.cs @@ -283,7 +283,7 @@ public void Test_Add_ImageSharp_WithSeparation() var separation = new Separation(PdfName.Get("SpotColor"), new RgbColor(0, 1, 0)); // Green // Act - var name = _pageResources.Add(image, separation, GrayScaleMethod.AverageOfRGBChannels); + var name = _pageResources.Add(image, separation, GrayScaleMethods.AverageOfRGBChannels); // Assert Assert.NotNull(name); From 90102637f19812d32f7dec9f3cb3418c5fc088d2 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Mon, 20 Apr 2026 15:37:23 +0200 Subject: [PATCH 2/2] Fixed missing xml comments --- .../Content/GrayScaleMethod.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs b/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs index 176fd31..5b5cd15 100644 --- a/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs +++ b/src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs @@ -95,5 +95,16 @@ public static GrayScaleMethod24 Threshold(GrayScaleMethod24 initialMethod, byte } } +/// +/// Represents a method to convert a pixel into a single byte. +/// +/// The pixel to convert +/// The byte value the pixel represents. public delegate byte GrayScaleMethod32(ref Rgba32 pixel); + +/// +/// Represents a method to convert a pixel into a single byte. +/// +/// The pixel to convert +/// The byte value the pixel represents. public delegate byte GrayScaleMethod24(ref Rgb24 pixel);