From 037d6ea582f4b187c64d38054f273fc4fbb5c5e3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Jun 2023 00:42:04 +1000 Subject: [PATCH] Allow preservation of png color palette --- src/ImageSharp/Formats/Png/PngDecoder.cs | 8 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 103 +++++++---- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 40 ++-- src/ImageSharp/Formats/Png/PngMetadata.cs | 40 +--- .../Formats/Png/PngScanlineProcessor.cs | 175 +++++++----------- .../Formats/Png/PngDecoderTests.cs | 6 +- .../Formats/Png/PngEncoderTests.cs | 35 +--- 7 files changed, 178 insertions(+), 229 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index f273ac2b98..d226451389 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -61,24 +61,24 @@ public sealed class PngDecoder : ImageDecoder case PngColorType.Grayscale: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index d1d29dca6b..eab80ecd92 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -172,21 +172,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (image is null) { this.InitializeImage(metadata, out image); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken); break; case PngChunkType.Palette: - byte[] pal = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(pal); - this.palette = pal; + this.palette = chunk.Data.GetSpan().ToArray(); break; case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); @@ -292,12 +291,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.SkipChunkDataAndCrc(chunk); break; + case PngChunkType.Palette: + this.palette = chunk.Data.GetSpan().ToArray(); + break; + case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); + // Spec says tRNS must be after PLTE so safe to exit. if (this.colorMetadataOnly) { goto EOF; @@ -370,6 +372,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals PngThrowHelper.ThrowNoHeader(); } + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); } finally @@ -766,9 +771,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.header, scanlineSpan, rowSpan, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -787,8 +790,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.header, scanlineSpan, rowSpan, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -800,9 +802,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -860,9 +860,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -885,8 +883,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, (uint)pixelOffset, (uint)increment, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -899,9 +896,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -924,10 +919,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } } + /// + /// Decodes and assigns the color palette to the metadata + /// + /// The palette buffer. + /// The alpha palette buffer. + /// The png metadata. + private static void AssignColorPalette(ReadOnlySpan palette, ReadOnlySpan alpha, PngMetadata pngMetadata) + { + if (palette.Length == 0) + { + return; + } + + Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()]; + ref Rgb24 paletteBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(palette)); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(Unsafe.Add(ref paletteBase, (uint)i)); + } + + if (alpha.Length > 0) + { + // The alpha chunk may contain as many transparency entries as there are palette entries + // (more than that would not make any sense) or as few as one. + for (int i = 0; i < alpha.Length; i++) + { + ref Color color = ref colorTable[i]; + color = color.WithAlpha(alpha[i] / 255F); + } + } + + pngMetadata.ColorTable = colorTable; + } + /// /// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// - /// The alpha tRNS array. + /// The alpha tRNS buffer. /// The png metadata. private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata) { @@ -941,16 +970,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); - pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc)); return; } byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); - pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb24(r, g, b)); } } else if (this.pngColorType == PngColorType.Grayscale) @@ -959,20 +986,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { if (this.header.BitDepth == 16) { - pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])); + pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]))); } else { - pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0)); + pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0))); } - - pngMetadata.HasTransparency = true; } } - else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0) - { - pngMetadata.HasTransparency = true; - } } /// @@ -1461,7 +1482,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. - if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency) + if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette) { chunk = new PngChunk(length, type); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index fb1d33277a..eb50299181 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -875,7 +875,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable // 4-byte unsigned integer of gamma * 100,000. uint gammaValue = (uint)(this.gamma * 100_000F); - BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue); + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue); this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); } @@ -889,7 +889,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) { - if (!pngMetadata.HasTransparency) + if (pngMetadata.TransparentColor is null) { return; } @@ -897,19 +897,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span alpha = this.chunkDataBuffer.Span; if (pngMetadata.ColorType == PngColorType.Rgb) { - if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) + if (this.use16Bit) { - Rgb48 rgb = pngMetadata.TransparentRgb48.Value; + Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel(); BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); } - else if (pngMetadata.TransparentRgb24.HasValue) + else { alpha.Clear(); - Rgb24 rgb = pngMetadata.TransparentRgb24.Value; + Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24(); alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; @@ -918,15 +918,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } else if (pngMetadata.ColorType == PngColorType.Grayscale) { - if (pngMetadata.TransparentL16.HasValue && this.use16Bit) + if (this.use16Bit) { - BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); + L16 l16 = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } - else if (pngMetadata.TransparentL8.HasValue) + else { + L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); alpha.Clear(); - alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; + alpha[1] = l8.PackedValue; this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } } @@ -1175,7 +1177,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable stream.Write(buffer); - uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer + uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer if (data.Length > 0 && length > 0) { @@ -1290,8 +1292,20 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } // Use the metadata to determine what quantization depth to use if no quantizer has been set. - IQuantizer quantizer = encoder.Quantizer - ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + IQuantizer quantizer = encoder.Quantizer; + if (quantizer is null) + { + PngMetadata metadata = image.Metadata.GetPngMetadata(); + if (metadata.ColorTable is not null) + { + // Use the provided palette in total. The caller is responsible for setting values. + quantizer = new PaletteQuantizer(metadata.ColorTable.Value); + } + else + { + quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + } + } // Create quantized frame returning the palette and set the bit depth. using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration()); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9ff3905fe1..8806c29b1a 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.PixelFormats; - namespace SixLabors.ImageSharp.Formats.Png; /// @@ -27,11 +25,12 @@ public class PngMetadata : IDeepCloneable this.ColorType = other.ColorType; this.Gamma = other.Gamma; this.InterlaceMethod = other.InterlaceMethod; - this.HasTransparency = other.HasTransparency; - this.TransparentL8 = other.TransparentL8; - this.TransparentL16 = other.TransparentL16; - this.TransparentRgb24 = other.TransparentRgb24; - this.TransparentRgb48 = other.TransparentRgb48; + this.TransparentColor = other.TransparentColor; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } for (int i = 0; i < other.TextData.Count; i++) { @@ -61,33 +60,14 @@ public class PngMetadata : IDeepCloneable public float Gamma { get; set; } /// - /// Gets or sets the Rgb24 transparent color. - /// This represents any color in an 8 bit Rgb24 encoded png that should be transparent. - /// - public Rgb24? TransparentRgb24 { get; set; } - - /// - /// Gets or sets the Rgb48 transparent color. - /// This represents any color in a 16 bit Rgb24 encoded png that should be transparent. - /// - public Rgb48? TransparentRgb48 { get; set; } - - /// - /// Gets or sets the 8 bit grayscale transparent color. - /// This represents any color in an 8 bit grayscale encoded png that should be transparent. - /// - public L8? TransparentL8 { get; set; } - - /// - /// Gets or sets the 16 bit grayscale transparent color. - /// This represents any color in a 16 bit grayscale encoded png that should be transparent. + /// Gets or sets the color table, if any. /// - public L16? TransparentL16 { get; set; } + public ReadOnlyMemory? ColorTable { get; set; } /// - /// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded. + /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded. /// - public bool HasTransparency { get; set; } + public Color? TransparentColor { get; set; } /// /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 04a23308cc..b0afd9975e 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -18,9 +18,7 @@ internal static class PngScanlineProcessor in PngHeader header, ReadOnlySpan scanlineSpan, Span rowSpan, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; @@ -28,7 +26,7 @@ internal static class PngScanlineProcessor ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -55,13 +53,14 @@ internal static class PngScanlineProcessor if (header.BitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; for (nuint x = 0; x < (uint)header.Width; x++, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = luminance; - source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; + source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue; pixel.FromLa32(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -69,13 +68,13 @@ internal static class PngScanlineProcessor } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); for (nuint x = 0; x < (uint)header.Width; x++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor); source.L = luminance; - source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; + source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromLa16(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -89,9 +88,7 @@ internal static class PngScanlineProcessor Span rowSpan, uint pixelOffset, uint increment, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; @@ -99,7 +96,7 @@ internal static class PngScanlineProcessor ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -126,13 +123,14 @@ internal static class PngScanlineProcessor if (header.BitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = luminance; - source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; + source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue; pixel.FromLa32(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -140,13 +138,13 @@ internal static class PngScanlineProcessor } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); source.L = luminance; - source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; + source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromLa16(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -241,11 +239,10 @@ internal static class PngScanlineProcessor in PngHeader header, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { - if (palette.IsEmpty) + if (palette is null) { PngThrowHelper.ThrowMissingPalette(); } @@ -253,36 +250,13 @@ internal static class PngScanlineProcessor TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); - ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); - if (paletteAlpha?.Length > 0) + for (nuint x = 0; x < (uint)header.Width; x++) { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - - for (nuint x = 0; x < (uint)header.Width; x++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, x); - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = 0; x < (uint)header.Width; x++) - { - int index = Unsafe.Add(ref scanlineSpanRef, x); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, x); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -292,42 +266,24 @@ internal static class PngScanlineProcessor Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { + if (palette is null) + { + PngThrowHelper.ThrowMissingPalette(); + } + TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); - ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); - if (paletteAlpha?.Length > 0) + for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, o); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) - { - int index = Unsafe.Add(ref scanlineSpanRef, o); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, o); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -338,15 +294,13 @@ internal static class PngScanlineProcessor Span rowSpan, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -372,6 +326,8 @@ internal static class PngScanlineProcessor if (header.BitDepth == 16) { + Rgb48 transparent = transparentColor.Value.ToPixel(); + Rgb48 rgb48 = default; Rgba64 rgba64 = default; int o = 0; @@ -382,7 +338,7 @@ internal static class PngScanlineProcessor rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -390,6 +346,8 @@ internal static class PngScanlineProcessor } else { + Rgb24 transparent = transparentColor.Value.ToPixel(); + Rgba32 rgba32 = default; ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); @@ -397,7 +355,7 @@ internal static class PngScanlineProcessor { ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x); rgba32.Rgb = rgb24; - rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue; + rgba32.A = rgb24.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromRgba32(rgba32); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -413,21 +371,19 @@ internal static class PngScanlineProcessor uint increment, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + bool hasTransparency = transparentColor is not null; - if (header.BitDepth == 16) + if (transparentColor is null) { - if (hasTrans) + if (header.BitDepth == 16) { Rgb48 rgb48 = default; - Rgba64 rgba64 = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) { @@ -435,24 +391,21 @@ internal static class PngScanlineProcessor rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); + pixel.FromRgb48(rgb48); Unsafe.Add(ref rowSpanRef, x) = pixel; } } else { - Rgb48 rgb48 = default; + Rgb24 rgb = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - pixel.FromRgb48(rgb48); + pixel.FromRgb24(rgb); Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -460,32 +413,40 @@ internal static class PngScanlineProcessor return; } - if (hasTrans) + if (header.BitDepth == 16) { - Rgba32 rgba = default; + Rgb48 transparent = transparentColor.Value.ToPixel(); + + Rgb48 rgb48 = default; + Rgba64 rgba64 = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) { - rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - pixel.FromRgba32(rgba); + rgba64.Rgb = rgb48; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; + + pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; } } else { - Rgb24 rgb = default; + Rgb24 transparent = transparentColor.Value.ToPixel(); + + Rgba32 rgba = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) { - rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + rgba.A = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; - pixel.FromRgb24(rgb); + pixel.FromRgba32(rgba); Unsafe.Add(ref rowSpanRef, x) = pixel; } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2dfd99439a..e216832853 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -539,7 +539,8 @@ public partial class PngDecoderTests { using Image image = provider.GetImage(PngDecoder.Instance); PngMetadata metadata = image.Metadata.GetPngMetadata(); - Assert.True(metadata.HasTransparency); + Assert.NotNull(metadata.ColorTable); + Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255); } // https://github.com/SixLabors/ImageSharp/issues/2209 @@ -551,7 +552,8 @@ public partial class PngDecoderTests using MemoryStream stream = new(testFile.Bytes, false); ImageInfo imageInfo = Image.Identify(stream); PngMetadata metadata = imageInfo.Metadata.GetPngMetadata(); - Assert.True(metadata.HasTransparency); + Assert.NotNull(metadata.ColorTable); + Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255); } // https://github.com/SixLabors/ImageSharp/issues/410 diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b20ec0675a..4e3d69bbfd 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -449,44 +449,15 @@ public partial class PngEncoderTests TestFile testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); PngMetadata inMeta = input.Metadata.GetPngMetadata(); - Assert.True(inMeta.HasTransparency); + Assert.True(inMeta.TransparentColor.HasValue); using MemoryStream memStream = new(); input.Save(memStream, PngEncoder); memStream.Position = 0; using Image output = Image.Load(memStream); PngMetadata outMeta = output.Metadata.GetPngMetadata(); - Assert.True(outMeta.HasTransparency); - - switch (pngColorType) - { - case PngColorType.Grayscale: - if (pngBitDepth.Equals(PngBitDepth.Bit16)) - { - Assert.True(outMeta.TransparentL16.HasValue); - Assert.Equal(inMeta.TransparentL16, outMeta.TransparentL16); - } - else - { - Assert.True(outMeta.TransparentL8.HasValue); - Assert.Equal(inMeta.TransparentL8, outMeta.TransparentL8); - } - - break; - case PngColorType.Rgb: - if (pngBitDepth.Equals(PngBitDepth.Bit16)) - { - Assert.True(outMeta.TransparentRgb48.HasValue); - Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48); - } - else - { - Assert.True(outMeta.TransparentRgb24.HasValue); - Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24); - } - - break; - } + Assert.True(outMeta.TransparentColor.HasValue); + Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor); } [Theory]