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: case PngColorType.Grayscale:
if (bits == PngBitDepth.Bit16) if (bits == PngBitDepth.Bit16)
{ {
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<L16>(options, stream, cancellationToken) ? this.Decode<L16>(options, stream, cancellationToken)
: this.Decode<La32>(options, stream, cancellationToken); : this.Decode<La32>(options, stream, cancellationToken);
} }
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<L8>(options, stream, cancellationToken) ? this.Decode<L8>(options, stream, cancellationToken)
: this.Decode<La16>(options, stream, cancellationToken); : this.Decode<La16>(options, stream, cancellationToken);
case PngColorType.Rgb: case PngColorType.Rgb:
if (bits == PngBitDepth.Bit16) if (bits == PngBitDepth.Bit16)
{ {
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<Rgb48>(options, stream, cancellationToken) ? this.Decode<Rgb48>(options, stream, cancellationToken)
: this.Decode<Rgba64>(options, stream, cancellationToken); : this.Decode<Rgba64>(options, stream, cancellationToken);
} }
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<Rgb24>(options, stream, cancellationToken) ? this.Decode<Rgb24>(options, stream, cancellationToken)
: this.Decode<Rgba32>(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) if (image is null)
{ {
this.InitializeImage(metadata, out image); 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); this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken);
break; break;
case PngChunkType.Palette: case PngChunkType.Palette:
byte[] pal = new byte[chunk.Length]; this.palette = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(pal);
this.palette = pal;
break; break;
case PngChunkType.Transparency: case PngChunkType.Transparency:
byte[] alpha = new byte[chunk.Length]; this.paletteAlpha = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(alpha); this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha, pngMetadata);
break; break;
case PngChunkType.Text: case PngChunkType.Text:
this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
@ -292,12 +291,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.SkipChunkDataAndCrc(chunk); this.SkipChunkDataAndCrc(chunk);
break; break;
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
case PngChunkType.Transparency: case PngChunkType.Transparency:
byte[] alpha = new byte[chunk.Length]; this.paletteAlpha = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(alpha); this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha, pngMetadata);
// Spec says tRNS must be after PLTE so safe to exit.
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
{ {
goto EOF; goto EOF;
@ -370,6 +372,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngThrowHelper.ThrowNoHeader(); 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); return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
} }
finally finally
@ -766,9 +771,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header, this.header,
scanlineSpan, scanlineSpan,
rowSpan, rowSpan,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
break; break;
@ -787,8 +790,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header, this.header,
scanlineSpan, scanlineSpan,
rowSpan, rowSpan,
this.palette, pngMetadata.ColorTable);
this.paletteAlpha);
break; break;
@ -800,9 +802,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
this.bytesPerPixel, this.bytesPerPixel,
this.bytesPerSample, this.bytesPerSample,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
break; break;
@ -860,9 +860,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
(uint)pixelOffset, (uint)pixelOffset,
(uint)increment, (uint)increment,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
break; break;
@ -885,8 +883,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
(uint)pixelOffset, (uint)pixelOffset,
(uint)increment, (uint)increment,
this.palette, pngMetadata.ColorTable);
this.paletteAlpha);
break; break;
@ -899,9 +896,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
(uint)increment, (uint)increment,
this.bytesPerPixel, this.bytesPerPixel,
this.bytesPerSample, this.bytesPerSample,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
break; 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> /// <summary>
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// Decodes and assigns marker colors that identify transparent pixels in non indexed images.
/// </summary> /// </summary>
/// <param name="alpha">The alpha tRNS array.</param> /// <param name="alpha">The alpha tRNS buffer.</param>
/// <param name="pngMetadata">The png metadata.</param> /// <param name="pngMetadata">The png metadata.</param>
private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngMetadata) 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 gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2));
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc));
pngMetadata.HasTransparency = true;
return; return;
} }
byte r = ReadByteLittleEndian(alpha, 0); byte r = ReadByteLittleEndian(alpha, 0);
byte g = ReadByteLittleEndian(alpha, 2); byte g = ReadByteLittleEndian(alpha, 2);
byte b = ReadByteLittleEndian(alpha, 4); byte b = ReadByteLittleEndian(alpha, 4);
pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); pngMetadata.TransparentColor = new(new Rgb24(r, g, b));
pngMetadata.HasTransparency = true;
} }
} }
else if (this.pngColorType == PngColorType.Grayscale) else if (this.pngColorType == PngColorType.Grayscale)
@ -959,20 +986,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
if (this.header.BitDepth == 16) if (this.header.BitDepth == 16)
{ {
pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])); pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])));
} }
else 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> /// <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. // 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. // 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); 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. // 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F); 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); 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> /// <param name="pngMetadata">The image metadata.</param>
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{ {
if (!pngMetadata.HasTransparency) if (pngMetadata.TransparentColor is null)
{ {
return; return;
} }
@ -897,19 +897,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> alpha = this.chunkDataBuffer.Span; Span<byte> alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb) 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, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
} }
else if (pngMetadata.TransparentRgb24.HasValue) else
{ {
alpha.Clear(); alpha.Clear();
Rgb24 rgb = pngMetadata.TransparentRgb24.Value; Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24();
alpha[1] = rgb.R; alpha[1] = rgb.R;
alpha[3] = rgb.G; alpha[3] = rgb.G;
alpha[5] = rgb.B; alpha[5] = rgb.B;
@ -918,15 +918,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
else if (pngMetadata.ColorType == PngColorType.Grayscale) 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); 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.Clear();
alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; alpha[1] = l8.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
} }
} }
@ -1175,7 +1177,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
stream.Write(buffer); 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) 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. // Use the metadata to determine what quantization depth to use if no quantizer has been set.
IQuantizer quantizer = encoder.Quantizer IQuantizer quantizer = encoder.Quantizer;
?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); 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. // Create quantized frame returning the palette and set the bit depth.
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration()); using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration());

40
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
@ -27,11 +25,12 @@ public class PngMetadata : IDeepCloneable
this.ColorType = other.ColorType; this.ColorType = other.ColorType;
this.Gamma = other.Gamma; this.Gamma = other.Gamma;
this.InterlaceMethod = other.InterlaceMethod; this.InterlaceMethod = other.InterlaceMethod;
this.HasTransparency = other.HasTransparency; this.TransparentColor = other.TransparentColor;
this.TransparentL8 = other.TransparentL8;
this.TransparentL16 = other.TransparentL16; if (other.ColorTable?.Length > 0)
this.TransparentRgb24 = other.TransparentRgb24; {
this.TransparentRgb48 = other.TransparentRgb48; this.ColorTable = other.ColorTable.Value.ToArray();
}
for (int i = 0; i < other.TextData.Count; i++) for (int i = 0; i < other.TextData.Count; i++)
{ {
@ -61,33 +60,14 @@ public class PngMetadata : IDeepCloneable
public float Gamma { get; set; } public float Gamma { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Rgb24 transparent color. /// Gets or sets the color table, if any.
/// 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.
/// </summary> /// </summary>
public L16? TransparentL16 { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary> /// <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> /// </summary>
public bool HasTransparency { get; set; } public Color? TransparentColor { get; set; }
/// <summary> /// <summary>
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. /// 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, in PngHeader header,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
bool hasTrans, Color? transparentColor)
L16 luminance16Trans,
L8 luminanceTrans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel pixel = default; TPixel pixel = default;
@ -28,7 +26,7 @@ internal static class PngScanlineProcessor
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans) if (transparentColor is null)
{ {
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
@ -55,13 +53,14 @@ internal static class PngScanlineProcessor
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
L16 transparent = transparentColor.Value.ToPixel<L16>();
La32 source = default; La32 source = default;
int o = 0; int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += 2) for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
{ {
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance; 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); pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -69,13 +68,13 @@ internal static class PngScanlineProcessor
} }
else else
{ {
byte transparent = (byte)(transparentColor.Value.ToPixel<L8>().PackedValue * scaleFactor);
La16 source = default; La16 source = default;
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = 0; x < (uint)header.Width; x++) for (nuint x = 0; x < (uint)header.Width; x++)
{ {
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor); byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
source.L = luminance; source.L = luminance;
source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source); pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -89,9 +88,7 @@ internal static class PngScanlineProcessor
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
uint increment, uint increment,
bool hasTrans, Color? transparentColor)
L16 luminance16Trans,
L8 luminanceTrans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel pixel = default; TPixel pixel = default;
@ -99,7 +96,7 @@ internal static class PngScanlineProcessor
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans) if (transparentColor is null)
{ {
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
@ -126,13 +123,14 @@ internal static class PngScanlineProcessor
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
L16 transparent = transparentColor.Value.ToPixel<L16>();
La32 source = default; La32 source = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2) for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
{ {
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance; 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); pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -140,13 +138,13 @@ internal static class PngScanlineProcessor
} }
else else
{ {
byte transparent = (byte)(transparentColor.Value.ToPixel<L8>().PackedValue * scaleFactor);
La16 source = default; La16 source = default;
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
{ {
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
source.L = luminance; source.L = luminance;
source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source); pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -241,11 +239,10 @@ internal static class PngScanlineProcessor
in PngHeader header, in PngHeader header,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
ReadOnlySpan<byte> palette, ReadOnlyMemory<Color>? palette)
byte[] paletteAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (palette.IsEmpty) if (palette is null)
{ {
PngThrowHelper.ThrowMissingPalette(); PngThrowHelper.ThrowMissingPalette();
} }
@ -253,36 +250,13 @@ internal static class PngScanlineProcessor
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette); ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
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 uint index = Unsafe.Add(ref scanlineSpanRef, x);
// channel and we should try to read it. pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
Rgba32 rgba = default; Unsafe.Add(ref rowSpanRef, x) = pixel;
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;
}
} }
} }
@ -292,42 +266,24 @@ internal static class PngScanlineProcessor
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
uint increment, uint increment,
ReadOnlySpan<byte> palette, ReadOnlyMemory<Color>? palette)
byte[] paletteAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (palette is null)
{
PngThrowHelper.ThrowMissingPalette();
}
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette); ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
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 uint index = Unsafe.Add(ref scanlineSpanRef, o);
// channel and we should try to read it. pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
Rgba32 rgba = default; Unsafe.Add(ref rowSpanRef, x) = pixel;
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;
}
} }
} }
@ -338,15 +294,13 @@ internal static class PngScanlineProcessor
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
int bytesPerPixel, int bytesPerPixel,
int bytesPerSample, int bytesPerSample,
bool hasTrans, Color? transparentColor)
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel pixel = default; TPixel pixel = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (!hasTrans) if (transparentColor is null)
{ {
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
@ -372,6 +326,8 @@ internal static class PngScanlineProcessor
if (header.BitDepth == 16) if (header.BitDepth == 16)
{ {
Rgb48 transparent = transparentColor.Value.ToPixel<Rgb48>();
Rgb48 rgb48 = default; Rgb48 rgb48 = default;
Rgba64 rgba64 = default; Rgba64 rgba64 = default;
int o = 0; int o = 0;
@ -382,7 +338,7 @@ internal static class PngScanlineProcessor
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48; rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64); pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -390,6 +346,8 @@ internal static class PngScanlineProcessor
} }
else else
{ {
Rgb24 transparent = transparentColor.Value.ToPixel<Rgb24>();
Rgba32 rgba32 = default; Rgba32 rgba32 = default;
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan); ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan);
ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); 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); ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
rgba32.Rgb = rgb24; rgba32.Rgb = rgb24;
rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue; rgba32.A = rgb24.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromRgba32(rgba32); pixel.FromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -413,21 +371,19 @@ internal static class PngScanlineProcessor
uint increment, uint increment,
int bytesPerPixel, int bytesPerPixel,
int bytesPerSample, int bytesPerSample,
bool hasTrans, Color? transparentColor)
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); 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; Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) 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.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48; pixel.FromRgb48(rgb48);
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
else else
{ {
Rgb48 rgb48 = default; Rgb24 rgb = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{ {
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
pixel.FromRgb48(rgb48); pixel.FromRgb24(rgb);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
@ -460,32 +413,40 @@ internal static class PngScanlineProcessor
return; 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; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{ {
rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
pixel.FromRgba32(rgba); rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
else else
{ {
Rgb24 rgb = default; Rgb24 transparent = transparentColor.Value.ToPixel<Rgb24>();
Rgba32 rgba = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{ {
rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * 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; 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); using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
PngMetadata metadata = image.Metadata.GetPngMetadata(); 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 // https://github.com/SixLabors/ImageSharp/issues/2209
@ -551,7 +552,8 @@ public partial class PngDecoderTests
using MemoryStream stream = new(testFile.Bytes, false); using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream); ImageInfo imageInfo = Image.Identify(stream);
PngMetadata metadata = imageInfo.Metadata.GetPngMetadata(); 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 // 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); TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
PngMetadata inMeta = input.Metadata.GetPngMetadata(); PngMetadata inMeta = input.Metadata.GetPngMetadata();
Assert.True(inMeta.HasTransparency); Assert.True(inMeta.TransparentColor.HasValue);
using MemoryStream memStream = new(); using MemoryStream memStream = new();
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
memStream.Position = 0; memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream); using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
PngMetadata outMeta = output.Metadata.GetPngMetadata(); PngMetadata outMeta = output.Metadata.GetPngMetadata();
Assert.True(outMeta.HasTransparency); Assert.True(outMeta.TransparentColor.HasValue);
Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor);
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;
}
} }
[Theory] [Theory]

Loading…
Cancel
Save