Browse Source

Merge pull request #2485 from SixLabors/js/png-pallete

Expose and conserve the color palette for indexed png images.
pull/2543/head
James Jackson-South 2 years ago
committed by GitHub
parent
commit
e905b0a330
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      src/ImageSharp/Color/Color.cs
  2. 8
      src/ImageSharp/Formats/Png/PngDecoder.cs
  3. 103
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  4. 11
      src/ImageSharp/Formats/Png/PngEncoder.cs
  5. 40
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 40
      src/ImageSharp/Formats/Png/PngMetadata.cs
  7. 175
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  8. 3
      src/ImageSharp/Memory/Buffer2D{T}.cs
  9. 12
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  10. 40
      tests/ImageSharp.Tests/Color/ColorTests.cs
  11. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  12. 37
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

12
src/ImageSharp/Color/Color.cs

@ -251,7 +251,17 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary>
/// <returns>A hexadecimal string representation of the value.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public string ToHex() => this.data.ToRgba32().ToHex();
public string ToHex()
{
if (this.boxedHighPrecisionPixel is not null)
{
Rgba32 rgba = default;
this.boxedHighPrecisionPixel.ToRgba32(ref rgba);
return rgba.ToHex();
}
return this.data.ToRgba32().ToHex();
}
/// <inheritdoc />
public override string ToString() => this.ToHex();

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>()];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(palette);
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(rgbTable[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);

11
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -3,6 +3,7 @@
#nullable disable
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
@ -11,6 +12,16 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary>
public class PngEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder()
// Hack. TODO: Investigate means to fix/optimize the Wu quantizer.
// The Wu quantizer does not handle the default sampling strategy well for some larger images.
// It's expensive and the results are not better than the extensive strategy.
=> this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy();
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType" /> values.

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;
}
}

3
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -9,9 +9,6 @@ namespace SixLabors.ImageSharp.Memory;
/// Represents a buffer of value type objects
/// interpreted as a 2D region of <see cref="Width"/> x <see cref="Height"/> elements.
/// </summary>
/// <remarks>
/// Before RC1, this class might be target of API changes, use it on your own risk!
/// </remarks>
/// <typeparam name="T">The value type.</typeparam>
public sealed class Buffer2D<T> : IDisposable
where T : struct

12
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs

@ -111,7 +111,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette
public readonly ReadOnlyMemory<TPixel> Palette
{
get
{
@ -362,7 +362,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="bounds">The bounds within the source image to quantize.</param>
private void Build3DHistogram(Buffer2D<TPixel> source, Rectangle bounds)
private readonly void Build3DHistogram(Buffer2D<TPixel> source, Rectangle bounds)
{
Span<Moment> momentSpan = this.momentsOwner.GetSpan();
@ -393,7 +393,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary>
/// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator allocator)
private readonly void Get3DMoments(MemoryAllocator allocator)
{
using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = allocator.Allocate<Moment>(IndexAlphaCount);
@ -462,7 +462,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary>
/// <param name="cube">The cube.</param>
/// <returns>The <see cref="float"/>.</returns>
private double Variance(ref Box cube)
private readonly double Variance(ref Box cube)
{
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
@ -503,7 +503,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// <param name="cut">The cutting point.</param>
/// <param name="whole">The whole moment.</param>
/// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole)
private readonly float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole)
{
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan);
@ -634,7 +634,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="label">A label.</param>
private void Mark(ref Box cube, byte label)
private readonly void Mark(ref Box cube, byte label)
{
Span<byte> tagSpan = this.tagsOwner.GetSpan();

40
tests/ImageSharp.Tests/Color/ColorTests.cs

@ -18,25 +18,42 @@ public partial class ColorTests
Assert.Equal(expected, (Rgba32)c2);
}
[Fact]
public void Equality_WhenTrue()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Equality_WhenTrue(bool highPrecision)
{
Color c1 = new Rgba64(100, 2000, 3000, 40000);
Color c2 = new Rgba64(100, 2000, 3000, 40000);
if (highPrecision)
{
c1 = Color.FromPixel(c1.ToPixel<RgbaVector>());
c2 = Color.FromPixel(c2.ToPixel<RgbaVector>());
}
Assert.True(c1.Equals(c2));
Assert.True(c1 == c2);
Assert.False(c1 != c2);
Assert.True(c1.GetHashCode() == c2.GetHashCode());
}
[Fact]
public void Equality_WhenFalse()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Equality_WhenFalse(bool highPrecision)
{
Color c1 = new Rgba64(100, 2000, 3000, 40000);
Color c2 = new Rgba64(101, 2000, 3000, 40000);
Color c3 = new Rgba64(100, 2000, 3000, 40001);
if (highPrecision)
{
c1 = Color.FromPixel(c1.ToPixel<RgbaVector>());
c2 = Color.FromPixel(c2.ToPixel<RgbaVector>());
c3 = Color.FromPixel(c3.ToPixel<RgbaVector>());
}
Assert.False(c1.Equals(c2));
Assert.False(c2.Equals(c3));
Assert.False(c3.Equals(c1));
@ -47,13 +64,20 @@ public partial class ColorTests
Assert.False(c1.Equals(null));
}
[Fact]
public void ToHex()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ToHex(bool highPrecision)
{
string expected = "ABCD1234";
var color = Color.ParseHex(expected);
string actual = color.ToHex();
Color color = Color.ParseHex(expected);
if (highPrecision)
{
color = Color.FromPixel(color.ToPixel<RgbaVector>());
}
string actual = color.ToHex();
Assert.Equal(expected, actual);
}

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

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

@ -449,44 +449,17 @@ 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);
Assert.Equal(pngBitDepth, outMeta.BitDepth);
Assert.Equal(pngColorType, outMeta.ColorType);
}
[Theory]

Loading…
Cancel
Save