Browse Source

Allow preservation of png color palette

pull/2485/head
James Jackson-South 3 years ago
parent
commit
037d6ea582
  1. 8
      src/ImageSharp/Formats/Png/PngDecoder.cs
  2. 103
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  3. 40
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 40
      src/ImageSharp/Formats/Png/PngMetadata.cs
  5. 175
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  6. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  7. 35
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

8
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<L16>(options, stream, cancellationToken)
: this.Decode<La32>(options, stream, cancellationToken);
}
return !meta.HasTransparency
return !meta.TransparentColor.HasValue
? this.Decode<L8>(options, stream, cancellationToken)
: this.Decode<La16>(options, stream, cancellationToken);
case PngColorType.Rgb:
if (bits == PngBitDepth.Bit16)
{
return !meta.HasTransparency
return !meta.TransparentColor.HasValue
? this.Decode<Rgb48>(options, stream, cancellationToken)
: this.Decode<Rgba64>(options, stream, cancellationToken);
}
return !meta.HasTransparency
return !meta.TransparentColor.HasValue
? this.Decode<Rgb24>(options, stream, cancellationToken)
: this.Decode<Rgba32>(options, stream, cancellationToken);

103
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
}
}
/// <summary>
/// Decodes and assigns the color palette to the metadata
/// </summary>
/// <param name="palette">The palette buffer.</param>
/// <param name="alpha">The alpha palette buffer.</param>
/// <param name="pngMetadata">The png metadata.</param>
private static void AssignColorPalette(ReadOnlySpan<byte> palette, ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
{
if (palette.Length == 0)
{
return;
}
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Rgb24>()];
ref Rgb24 paletteBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(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;
}
/// <summary>
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images.
/// </summary>
/// <param name="alpha">The alpha tRNS array.</param>
/// <param name="alpha">The alpha tRNS buffer.</param>
/// <param name="pngMetadata">The png metadata.</param>
private void AssignTransparentMarkers(ReadOnlySpan<byte> 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;
}
}
/// <summary>
@ -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);

40
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
/// <param name="pngMetadata">The image metadata.</param>
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<byte> 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<Rgb48>();
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<L16>();
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<L8>();
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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration());

40
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;
/// <summary>
@ -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; }
/// <summary>
/// Gets or sets the Rgb24 transparent color.
/// This represents any color in an 8 bit Rgb24 encoded png that should be transparent.
/// </summary>
public Rgb24? TransparentRgb24 { get; set; }
/// <summary>
/// Gets or sets the Rgb48 transparent color.
/// This represents any color in a 16 bit Rgb24 encoded png that should be transparent.
/// </summary>
public Rgb48? TransparentRgb48 { get; set; }
/// <summary>
/// Gets or sets the 8 bit grayscale transparent color.
/// This represents any color in an 8 bit grayscale encoded png that should be transparent.
/// </summary>
public L8? TransparentL8 { get; set; }
/// <summary>
/// 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.
/// </summary>
public L16? TransparentL16 { get; set; }
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool HasTransparency { get; set; }
public Color? TransparentColor { get; set; }
/// <summary>
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks.

175
src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

@ -18,9 +18,7 @@ internal static class PngScanlineProcessor
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
bool hasTrans,
L16 luminance16Trans,
L8 luminanceTrans)
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<L16>();
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<L8>().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<TPixel> rowSpan,
uint pixelOffset,
uint increment,
bool hasTrans,
L16 luminance16Trans,
L8 luminanceTrans)
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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<L16>();
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<L8>().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<byte> scanlineSpan,
Span<TPixel> rowSpan,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
ReadOnlyMemory<Color>? palette)
where TPixel : unmanaged, IPixel<TPixel>
{
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<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(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<TPixel> rowSpan,
uint pixelOffset,
uint increment,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
ReadOnlyMemory<Color>? palette)
where TPixel : unmanaged, IPixel<TPixel>
{
if (palette is null)
{
PngThrowHelper.ThrowMissingPalette();
}
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(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<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
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 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<Rgb24>();
Rgba32 rgba32 = default;
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(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>
{
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 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<Rgb24>();
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;
}
}

6
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -539,7 +539,8 @@ public partial class PngDecoderTests
{
using Image<TPixel> 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

35
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -449,44 +449,15 @@ public partial class PngEncoderTests
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> 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<Rgba32> output = Image.Load<Rgba32>(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]

Loading…
Cancel
Save