diff --git a/PngSharp.Tests/GammaCorrectionTests.cs b/PngSharp.Tests/GammaCorrectionTests.cs new file mode 100644 index 0000000..266a90d --- /dev/null +++ b/PngSharp.Tests/GammaCorrectionTests.cs @@ -0,0 +1,500 @@ +using PngSharp.Api; +using PngSharp.Spec.Chunks.IHDR; +using PngSharp.Spec.Chunks.PLTE; +using PngSharp.Spec.Chunks.sGAMA; +using PngSharp.Spec.Chunks.sRGB; +using Xunit; +using static PngSharp.Tests.PngTestHelpers; + +namespace PngSharp.Tests; + +public class GammaCorrectionTests +{ + // --- GetFileGamma --- + + [Fact] + public void GetFileGamma_WithSrgbChunk_ReturnsApprox0_45455() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var gamma = png.GetFileGamma(); + + Assert.NotNull(gamma); + Assert.Equal(1.0 / 2.2, gamma!.Value, precision: 5); + } + + [Fact] + public void GetFileGamma_WithGamaChunk_ReturnsGamaValue() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + var gamma = png.GetFileGamma(); + + Assert.NotNull(gamma); + Assert.Equal(0.45455, gamma!.Value, precision: 5); + } + + [Fact] + public void GetFileGamma_WithGamaChunk_LinearGamma() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithGama(GammaChunkData.FromDouble(1.0)) + .Build(); + + var gamma = png.GetFileGamma(); + + Assert.NotNull(gamma); + Assert.Equal(1.0, gamma!.Value, precision: 5); + } + + [Fact] + public void GetFileGamma_SrgbTakesPrecedenceOverGama() + { + // sRGB + gAMA is allowed (spec says encoders should include gAMA for compatibility) + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .WithGama(GammaChunkData.FromDouble(1.8)) // different value + .Build(); + + var gamma = png.GetFileGamma(); + + Assert.NotNull(gamma); + Assert.Equal(1.0 / 2.2, gamma!.Value, precision: 5); + } + + [Fact] + public void GetFileGamma_NoGammaChunks_ReturnsNull() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .Build(); + + Assert.Null(png.GetFileGamma()); + } + + // --- ApplyGammaCorrection --- + + [Fact] + public void ApplyGammaCorrection_IdentityGamma_NoChange() + { + // fileGamma (1/2.2) * displayGamma (2.2) = 1.0 -> exponent = 1.0 -> identity + byte[] pixels = [100, 150, 200, 50, 75, 25, 200, 100, 50, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(new GammaChunkData { Value = 45455 }) // 1/2.2 + .Build(); + + var result = png.ApplyGammaCorrection(2.2); + + Assert.Equal(pixels, result); + } + + [Fact] + public void ApplyGammaCorrection_TrueColorWithAlpha_AlphaPreserved() + { + byte[] pixels = [128, 64, 32, 200, 64, 128, 32, 100, 255, 0, 128, 50, 0, 255, 64, 80]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(0.5)) + .Build(); + + var result = png.ApplyGammaCorrection(1.0); + + // Alpha bytes (index 3, 7, 11, 15) must be unchanged + Assert.Equal(200, result[3]); + Assert.Equal(100, result[7]); + Assert.Equal(50, result[11]); + Assert.Equal(80, result[15]); + } + + [Fact] + public void ApplyGammaCorrection_Grayscale_CorrectValues() + { + byte[] pixels = [0, 128, 255, 64]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.Grayscale)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(0.5)) + .Build(); + + var result = png.ApplyGammaCorrection(1.0); + + // exponent = 1/(0.5*1.0) = 2.0 + // 0^2 = 0, 255^2/255 = 255 (fixed points) + Assert.Equal(0, result[0]); + Assert.Equal(255, result[2]); + // 128/255 = 0.502 -> 0.502^2 = 0.252 -> 0.252*255 = 64 + Assert.Equal(64, result[1]); + } + + [Fact] + public void ApplyGammaCorrection_GrayscaleWithAlpha_AlphaPreserved() + { + byte[] pixels = [128, 200, 64, 150, 255, 100, 0, 50]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.GrayscaleWithAlpha)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(0.5)) + .Build(); + + var result = png.ApplyGammaCorrection(1.0); + + // Alpha at indices 1, 3, 5, 7 must be unchanged + Assert.Equal(200, result[1]); + Assert.Equal(150, result[3]); + Assert.Equal(100, result[5]); + Assert.Equal(50, result[7]); + } + + [Fact] + public void ApplyGammaCorrection_NoGammaInfo_Throws() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .Build(); + + Assert.Throws(() => png.ApplyGammaCorrection()); + } + + [Fact] + public void ApplyGammaCorrection_ZeroDisplayGamma_Throws() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + Assert.Throws(() => png.ApplyGammaCorrection(0)); + } + + [Fact] + public void ApplyGammaCorrection_NegativeDisplayGamma_Throws() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + Assert.Throws(() => png.ApplyGammaCorrection(-1.0)); + } + + [Fact] + public void ApplyGammaCorrection_ReturnsNewArray() + { + byte[] pixels = [100, 150, 200, 50, 75, 25, 200, 100, 50, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + var result = png.ApplyGammaCorrection(); + + Assert.NotSame(png.PixelData, result); + } + + [Fact] + public void ApplyGammaCorrection_BlackAndWhite_AlwaysFixedPoints() + { + byte[] pixels = [0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(0.3)) + .Build(); + + var result = png.ApplyGammaCorrection(1.5); + + Assert.Equal(0, result[0]); + Assert.Equal(0, result[1]); + Assert.Equal(0, result[2]); + Assert.Equal(255, result[3]); + Assert.Equal(255, result[4]); + Assert.Equal(255, result[5]); + } + + [Fact] + public void ApplyGammaCorrection_SinglePixel() + { + byte[] pixels = [128, 64, 32, 255]; + var png = Png.Builder() + .WithIhdr(new IhdrChunkData + { + Width = 1, Height = 1, BitDepth = 8, + ColorType = ColorType.TrueColorWithAlpha, + CompressionMethod = CompressionMethod.DeflateWithSlidingWindow, + FilterMethod = FilterMethod.AdaptiveFiltering, + InterlaceMethod = InterlaceMethod.None, + }) + .WithPixelData(pixels) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + var result = png.ApplyGammaCorrection(); + + Assert.Equal(4, result.Length); + Assert.Equal(255, result[3]); // alpha preserved + } + + [Fact] + public void ApplyGammaCorrection_IndexedColor_ExpandsToRgb() + { + byte[] paletteEntries = [255, 0, 0, 0, 255, 0]; // red, green + byte[] pixels = [0, 1, 1, 0]; // indices into palette + + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.IndexedColor)) + .WithPixelData(pixels) + .WithPlte(new PlteChunkData { Entries = paletteEntries }) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + var result = png.ApplyGammaCorrection(); + + // 4 pixels * 3 bytes/pixel = 12 bytes (expanded from indexed) + Assert.Equal(12, result.Length); + } + + // --- ToLinear --- + + [Fact] + public void ToLinear_WithSrgb_UsesExactTransferFunction() + { + // sRGB 188 -> linear ~0.5 -> byte ~128 + byte[] pixels = [188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188, 188]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var result = png.ToLinear(); + + // sRGB 188/255 = 0.7373 -> linear = ((0.7373+0.055)/1.055)^2.4 ≈ 0.5028 -> byte ≈ 128 + Assert.InRange(result[0], 126, 130); + } + + [Fact] + public void ToLinear_WithGama_UsesPowerLaw() + { + byte[] pixels = [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(new GammaChunkData { Value = 45455 }) // ~1/2.2 + .Build(); + + var result = png.ToLinear(); + + // 128/255 = 0.502 -> (0.502)^(1/0.45455) = (0.502)^2.2 ≈ 0.218 -> byte ≈ 56 + Assert.InRange(result[0], 54, 58); + } + + [Fact] + public void ToLinear_AlphaPreserved() + { + byte[] pixels = [128, 200, 128, 150, 128, 100, 128, 50]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.GrayscaleWithAlpha)) + .WithPixelData(pixels) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var result = png.ToLinear(); + + Assert.Equal(200, result[1]); + Assert.Equal(150, result[3]); + Assert.Equal(100, result[5]); + Assert.Equal(50, result[7]); + } + + [Fact] + public void ToLinear_NoGammaInfo_Throws() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .Build(); + + Assert.Throws(() => png.ToLinear()); + } + + [Fact] + public void ToLinear_BlackAndWhite_FixedPoints() + { + byte[] pixels = [0, 255, 0, 255, 255, 128, 128, 64]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.GrayscaleWithAlpha)) + .WithPixelData(pixels) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var result = png.ToLinear(); + + Assert.Equal(0, result[0]); + Assert.Equal(255, result[4]); + } + + // --- ToSrgb --- + + [Fact] + public void ToSrgb_AlreadySrgb_ReturnsCopy() + { + byte[] pixels = [100, 150, 200, 50, 75, 25, 200, 100, 50, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var result = png.ToSrgb(); + + Assert.Equal(pixels, result); + Assert.NotSame(png.PixelData, result); + } + + [Fact] + public void ToSrgb_FromLinearGamma_CorrectValues() + { + // Linear data (gAMA = 1.0) -> sRGB encoding + // Linear 128/255 = 0.502 -> sRGB = 1.055*(0.502)^(1/2.4) - 0.055 ≈ 0.735 -> byte ≈ 188 + byte[] pixels = [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(1.0)) + .Build(); + + var result = png.ToSrgb(); + + Assert.InRange(result[0], 186, 190); + } + + [Fact] + public void ToSrgb_ReturnsNewArray() + { + byte[] pixels = [100, 150, 200, 50, 75, 25, 200, 100, 50, 128, 128, 128]; + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(pixels) + .WithGama(GammaChunkData.FromDouble(1.0)) + .Build(); + + var result = png.ToSrgb(); + + Assert.NotSame(png.PixelData, result); + } + + [Fact] + public void ToSrgb_NoGammaInfo_Throws() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .Build(); + + Assert.Throws(() => png.ToSrgb()); + } + + // --- GetGammaCorrectedPalette --- + + [Fact] + public void GetGammaCorrectedPalette_ReturnsCorrectedEntries() + { + byte[] paletteEntries = [0, 0, 0, 255, 255, 255, 128, 64, 32]; + byte[] pixels = [0, 1, 2, 0]; + + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.IndexedColor)) + .WithPixelData(pixels) + .WithPlte(new PlteChunkData { Entries = paletteEntries }) + .WithGama(GammaChunkData.FromDouble(0.5)) + .Build(); + + var result = png.GetGammaCorrectedPalette(1.0); + + Assert.NotNull(result); + var entries = result!.Value.Entries; + Assert.Equal(9, entries.Length); + // Black and white are fixed points + Assert.Equal(0, entries[0]); + Assert.Equal(0, entries[1]); + Assert.Equal(0, entries[2]); + Assert.Equal(255, entries[3]); + Assert.Equal(255, entries[4]); + Assert.Equal(255, entries[5]); + } + + [Fact] + public void GetGammaCorrectedPalette_NoPalette_ReturnsNull() + { + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.TrueColor)) + .WithPixelData(new byte[2 * 2 * 3]) + .WithGama(new GammaChunkData { Value = 45455 }) + .Build(); + + Assert.Null(png.GetGammaCorrectedPalette()); + } + + [Fact] + public void GetGammaCorrectedPalette_NoGammaInfo_Throws() + { + byte[] paletteEntries = [255, 0, 0, 0, 255, 0]; + byte[] pixels = [0, 1, 1, 0]; + + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.IndexedColor)) + .WithPixelData(pixels) + .WithPlte(new PlteChunkData { Entries = paletteEntries }) + .Build(); + + Assert.Throws(() => png.GetGammaCorrectedPalette()); + } + + // --- ToSrgb with IndexedColor --- + + [Fact] + public void ToSrgb_IndexedColor_WithSrgb_ExpandsToRgb() + { + byte[] paletteEntries = [255, 0, 0, 0, 255, 0]; + byte[] pixels = [0, 1, 1, 0]; + + var png = Png.Builder() + .WithIhdr(CreateIhdr(ColorType.IndexedColor)) + .WithPixelData(pixels) + .WithPlte(new PlteChunkData { Entries = paletteEntries }) + .WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual }) + .Build(); + + var result = png.ToSrgb(); + + Assert.Equal(12, result.Length); // 4 pixels * 3 bytes + // First pixel: palette[0] = red + Assert.Equal(255, result[0]); + Assert.Equal(0, result[1]); + Assert.Equal(0, result[2]); + // Second pixel: palette[1] = green + Assert.Equal(0, result[3]); + Assert.Equal(255, result[4]); + Assert.Equal(0, result[5]); + } +} diff --git a/PngSharp/Api/Png.cs b/PngSharp/Api/Png.cs index e9a521a..a7fe6b6 100644 --- a/PngSharp/Api/Png.cs +++ b/PngSharp/Api/Png.cs @@ -2,6 +2,7 @@ using PngSharp.Encoder; using PngSharp.Spec; using PngSharp.Spec.Chunks.IHDR; +using PngSharp.Spec.Chunks.PLTE; namespace PngSharp.Api; @@ -243,6 +244,127 @@ public static bool IsGrayscale(this IRawPng png) return png.Ihdr.ColorType is ColorType.Grayscale or ColorType.GrayscaleWithAlpha; } + /// + /// Returns the effective file gamma resolved from chunk precedence: sRGB > gAMA. + /// Returns null if no gamma information is available (no sRGB or gAMA chunk), + /// or if only an iCCP chunk is present (ICC profile parsing is not supported). + /// + public static double? GetFileGamma(this IRawPng png) + { + if (png.Srgb.HasValue) + return 1.0 / 2.2; + if (png.Gama.HasValue) + return png.Gama.Value.ToDouble(); + return null; + } + + /// + /// Applies PNG spec gamma correction to the pixel data. + /// Formula: output = input ^ (1.0 / (fileGamma * displayGamma)) + /// Alpha channels are never corrected. For indexed color images, the pixel data + /// is expanded to RGB (3 bytes per pixel) with corrected palette values. + /// + /// The PNG image + /// The display gamma exponent (default 2.2 for sRGB monitors) + /// A new byte array with gamma-corrected pixel data + public static byte[] ApplyGammaCorrection(this IRawPng png, double displayGamma = 2.2) + { + GammaUtils.GuardBitDepth(png); + if (displayGamma <= 0) + throw new ArgumentOutOfRangeException(nameof(displayGamma), "Display gamma must be greater than zero."); + + var fileGamma = png.GetFileGamma() + ?? throw new InvalidOperationException( + "No gamma information available. The image has no sRGB or gAMA chunk."); + + var exponent = 1.0 / (fileGamma * displayGamma); + var lut = GammaUtils.BuildLut(c => Math.Pow(c, exponent)); + return GammaUtils.ApplyLutToPixels(png, lut); + } + + /// + /// Converts pixel data to linear light (gamma 1.0). + /// Uses the precise sRGB piecewise transfer function when an sRGB chunk is present, + /// otherwise uses power-law inversion from the gAMA chunk. + /// Alpha channels are never corrected. For indexed color images, the pixel data + /// is expanded to RGB (3 bytes per pixel). + /// + public static byte[] ToLinear(this IRawPng png) + { + GammaUtils.GuardBitDepth(png); + + byte[] lut; + if (png.Srgb.HasValue) + { + lut = GammaUtils.BuildLut(GammaUtils.SrgbToLinear); + } + else if (png.Gama.HasValue) + { + var fileGamma = png.Gama.Value.ToDouble(); + var exponent = 1.0 / fileGamma; + lut = GammaUtils.BuildLut(c => Math.Pow(c, exponent)); + } + else + { + throw new InvalidOperationException( + "No gamma information available. The image has no sRGB or gAMA chunk."); + } + + return GammaUtils.ApplyLutToPixels(png, lut); + } + + /// + /// Converts pixel data to sRGB encoding. + /// If the image already has an sRGB chunk, returns a clone of the pixel data. + /// Otherwise linearizes via the gAMA value and applies the linear-to-sRGB transfer function. + /// Alpha channels are never corrected. For indexed color images, the pixel data + /// is expanded to RGB (3 bytes per pixel). + /// + public static byte[] ToSrgb(this IRawPng png) + { + GammaUtils.GuardBitDepth(png); + + if (png.Srgb.HasValue) + { + if (png.Ihdr.ColorType == ColorType.IndexedColor) + return GammaUtils.ExpandIndexedToRgb(png, null); + return (byte[])png.PixelData.Clone(); + } + + if (png.Gama.HasValue) + { + var fileGamma = png.Gama.Value.ToDouble(); + var exponent = 1.0 / fileGamma; + var lut = GammaUtils.BuildLut(c => GammaUtils.LinearToSrgb(Math.Pow(c, exponent))); + return GammaUtils.ApplyLutToPixels(png, lut); + } + + throw new InvalidOperationException( + "No gamma information available. The image has no sRGB or gAMA chunk."); + } + + /// + /// Returns a new with gamma-corrected RGB entries. + /// Returns null if the image has no palette. + /// + /// The PNG image + /// The display gamma exponent (default 2.2 for sRGB monitors) + public static PlteChunkData? GetGammaCorrectedPalette(this IRawPng png, double displayGamma = 2.2) + { + if (!png.Plte.HasValue) + return null; + if (displayGamma <= 0) + throw new ArgumentOutOfRangeException(nameof(displayGamma), "Display gamma must be greater than zero."); + + var fileGamma = png.GetFileGamma() + ?? throw new InvalidOperationException( + "No gamma information available. The image has no sRGB or gAMA chunk."); + + var exponent = 1.0 / (fileGamma * displayGamma); + var lut = GammaUtils.BuildLut(c => Math.Pow(c, exponent)); + return GammaUtils.CorrectPalette(png.Plte.Value, lut); + } + private sealed class NullLogger : ILogger { public void Debug(string message) { } diff --git a/PngSharp/Spec/GammaUtils.cs b/PngSharp/Spec/GammaUtils.cs new file mode 100644 index 0000000..da910ee --- /dev/null +++ b/PngSharp/Spec/GammaUtils.cs @@ -0,0 +1,97 @@ +using PngSharp.Api; +using PngSharp.Spec.Chunks.IHDR; +using PngSharp.Spec.Chunks.PLTE; + +namespace PngSharp.Spec; + +internal static class GammaUtils +{ + internal static void GuardBitDepth(IRawPng png) + { + if (png.Ihdr.BitDepth != 8) + throw new InvalidOperationException( + $"Gamma correction is only supported for 8-bit images. This image has {png.Ihdr.BitDepth}-bit depth."); + } + + internal static byte[] BuildLut(Func transfer) + { + var lut = new byte[256]; + for (var i = 0; i < 256; i++) + { + var normalized = i / 255.0; + var corrected = transfer(normalized); + lut[i] = (byte)Math.Clamp(Math.Round(corrected * 255.0), 0, 255); + } + return lut; + } + + internal static double SrgbToLinear(double c) + { + return c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); + } + + internal static double LinearToSrgb(double c) + { + return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.Pow(c, 1.0 / 2.4) - 0.055; + } + + internal static PlteChunkData CorrectPalette(PlteChunkData plte, byte[] lut) + { + var entries = plte.Entries; + var corrected = new byte[entries.Length]; + for (var i = 0; i < entries.Length; i++) + corrected[i] = lut[entries[i]]; + return new PlteChunkData { Entries = corrected }; + } + + internal static byte[] ExpandIndexedToRgb(IRawPng png, byte[]? lut) + { + var palette = png.Plte!.Value.Entries; + var pixelData = png.PixelData; + var result = new byte[pixelData.Length * 3]; + for (var i = 0; i < pixelData.Length; i++) + { + var idx = pixelData[i] * 3; + var outIdx = i * 3; + if (lut != null) + { + result[outIdx] = lut[palette[idx]]; + result[outIdx + 1] = lut[palette[idx + 1]]; + result[outIdx + 2] = lut[palette[idx + 2]]; + } + else + { + result[outIdx] = palette[idx]; + result[outIdx + 1] = palette[idx + 1]; + result[outIdx + 2] = palette[idx + 2]; + } + } + return result; + } + + internal static byte[] ApplyLutToPixels(IRawPng png, byte[] lut) + { + if (png.Ihdr.ColorType == ColorType.IndexedColor) + return ExpandIndexedToRgb(png, lut); + + var result = (byte[])png.PixelData.Clone(); + var bytesPerPixel = png.Ihdr.GetBytesPerPixel(); + + var colorChannels = png.Ihdr.ColorType switch + { + ColorType.Grayscale => 1, + ColorType.TrueColor => 3, + ColorType.GrayscaleWithAlpha => 1, + ColorType.TrueColorWithAlpha => 3, + _ => throw new InvalidOperationException($"Unsupported color type: {png.Ihdr.ColorType}") + }; + + for (var i = 0; i < result.Length; i += bytesPerPixel) + { + for (var c = 0; c < colorChannels; c++) + result[i + c] = lut[result[i + c]]; + } + + return result; + } +}