diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs
index 13af25f6c7..cebceabe09 100644
--- a/src/ImageSharp/Color/Color.cs
+++ b/src/ImageSharp/Color/Color.cs
@@ -251,7 +251,17 @@ public Color WithAlpha(float alpha)
///
/// A hexadecimal string representation of the value.
[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();
+ }
///
public override string ToString() => this.ToHex();
diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs
index f273ac2b98..d226451389 100644
--- a/src/ImageSharp/Formats/Png/PngDecoder.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoder.cs
@@ -61,24 +61,24 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat
case PngColorType.Grayscale:
if (bits == PngBitDepth.Bit16)
{
- return !meta.HasTransparency
+ return !meta.TransparentColor.HasValue
? this.Decode(options, stream, cancellationToken)
: this.Decode(options, stream, cancellationToken);
}
- return !meta.HasTransparency
+ return !meta.TransparentColor.HasValue
? this.Decode(options, stream, cancellationToken)
: this.Decode(options, stream, cancellationToken);
case PngColorType.Rgb:
if (bits == PngBitDepth.Bit16)
{
- return !meta.HasTransparency
+ return !meta.TransparentColor.HasValue
? this.Decode(options, stream, cancellationToken)
: this.Decode(options, stream, cancellationToken);
}
- return !meta.HasTransparency
+ return !meta.TransparentColor.HasValue
? this.Decode(options, stream, cancellationToken)
: this.Decode(options, stream, cancellationToken);
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index d1d29dca6b..065d861e71 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -172,21 +172,20 @@ public Image Decode(BufferedReadStream stream, CancellationToken
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 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
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 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
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 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
this.header,
scanlineSpan,
rowSpan,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentL16.GetValueOrDefault(),
- pngMetadata.TransparentL8.GetValueOrDefault());
+ pngMetadata.TransparentColor);
break;
@@ -787,8 +790,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
this.header,
scanlineSpan,
rowSpan,
- this.palette,
- this.paletteAlpha);
+ pngMetadata.ColorTable);
break;
@@ -800,9 +802,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan
rowSpan,
this.bytesPerPixel,
this.bytesPerSample,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentRgb48.GetValueOrDefault(),
- pngMetadata.TransparentRgb24.GetValueOrDefault());
+ pngMetadata.TransparentColor);
break;
@@ -860,9 +860,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
rowSpan,
(uint)pixelOffset,
(uint)increment,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentL16.GetValueOrDefault(),
- pngMetadata.TransparentL8.GetValueOrDefault());
+ pngMetadata.TransparentColor);
break;
@@ -885,8 +883,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
rowSpan,
(uint)pixelOffset,
(uint)increment,
- this.palette,
- this.paletteAlpha);
+ pngMetadata.ColorTable);
break;
@@ -899,9 +896,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
(uint)increment,
this.bytesPerPixel,
this.bytesPerSample,
- pngMetadata.HasTransparency,
- pngMetadata.TransparentRgb48.GetValueOrDefault(),
- pngMetadata.TransparentRgb24.GetValueOrDefault());
+ pngMetadata.TransparentColor);
break;
@@ -924,10 +919,44 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi
}
}
+ ///
+ /// Decodes and assigns the color palette to the metadata
+ ///
+ /// The palette buffer.
+ /// The alpha palette buffer.
+ /// The png metadata.
+ private static void AssignColorPalette(ReadOnlySpan palette, ReadOnlySpan alpha, PngMetadata pngMetadata)
+ {
+ if (palette.Length == 0)
+ {
+ return;
+ }
+
+ Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()];
+ ReadOnlySpan rgbTable = MemoryMarshal.Cast(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;
+ }
+
///
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images.
///
- /// The alpha tRNS array.
+ /// The alpha tRNS buffer.
/// The png metadata.
private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata)
{
@@ -941,16 +970,14 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM
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 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM
{
if (this.header.BitDepth == 16)
{
- pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]));
+ pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])));
}
else
{
- pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0));
+ pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0)));
}
-
- pngMetadata.HasTransparency = true;
}
}
- else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0)
- {
- pngMetadata.HasTransparency = true;
- }
}
///
@@ -1461,7 +1482,7 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk)
// If we're reading color metadata only we're only interested in the IHDR and tRNS chunks.
// We can skip all other chunk data in the stream for better performance.
- if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency)
+ if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette)
{
chunk = new PngChunk(length, type);
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index 595601522e..e8c88055e7 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/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;
///
public class PngEncoder : QuantizingImageEncoder
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ 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();
+
///
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all values.
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 175a9f777d..338bc7cff3 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -875,7 +875,7 @@ private void WriteGammaChunk(Stream stream)
// 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 @@ private void WriteGammaChunk(Stream stream)
/// The image metadata.
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
- if (!pngMetadata.HasTransparency)
+ if (pngMetadata.TransparentColor is null)
{
return;
}
@@ -897,19 +897,19 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
Span alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb)
{
- if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit)
+ if (this.use16Bit)
{
- Rgb48 rgb = pngMetadata.TransparentRgb48.Value;
+ Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
}
- else if (pngMetadata.TransparentRgb24.HasValue)
+ else
{
alpha.Clear();
- Rgb24 rgb = pngMetadata.TransparentRgb24.Value;
+ Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24();
alpha[1] = rgb.R;
alpha[3] = rgb.G;
alpha[5] = rgb.B;
@@ -918,15 +918,17 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
}
else if (pngMetadata.ColorType == PngColorType.Grayscale)
{
- if (pngMetadata.TransparentL16.HasValue && this.use16Bit)
+ if (this.use16Bit)
{
- BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue);
+ L16 l16 = pngMetadata.TransparentColor.Value.ToPixel();
+ BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
}
- else if (pngMetadata.TransparentL8.HasValue)
+ else
{
+ L8 l8 = pngMetadata.TransparentColor.Value.ToPixel();
alpha.Clear();
- alpha[1] = pngMetadata.TransparentL8.Value.PackedValue;
+ alpha[1] = l8.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
}
}
@@ -1175,7 +1177,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o
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 @@ private static IndexedImageFrame CreateQuantizedFrame(
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
- IQuantizer quantizer = encoder.Quantizer
- ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
+ IQuantizer quantizer = encoder.Quantizer;
+ if (quantizer is null)
+ {
+ PngMetadata metadata = image.Metadata.GetPngMetadata();
+ if (metadata.ColorTable is not null)
+ {
+ // Use the provided palette in total. The caller is responsible for setting values.
+ quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
+ }
+ else
+ {
+ quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
+ }
+ }
// Create quantized frame returning the palette and set the bit depth.
using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration());
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 9ff3905fe1..8806c29b1a 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.PixelFormats;
-
namespace SixLabors.ImageSharp.Formats.Png;
///
@@ -27,11 +25,12 @@ private PngMetadata(PngMetadata other)
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 @@ private PngMetadata(PngMetadata other)
public float Gamma { get; set; }
///
- /// Gets or sets the Rgb24 transparent color.
- /// This represents any color in an 8 bit Rgb24 encoded png that should be transparent.
- ///
- public Rgb24? TransparentRgb24 { get; set; }
-
- ///
- /// Gets or sets the Rgb48 transparent color.
- /// This represents any color in a 16 bit Rgb24 encoded png that should be transparent.
- ///
- public Rgb48? TransparentRgb48 { get; set; }
-
- ///
- /// Gets or sets the 8 bit grayscale transparent color.
- /// This represents any color in an 8 bit grayscale encoded png that should be transparent.
- ///
- public L8? TransparentL8 { get; set; }
-
- ///
- /// Gets or sets the 16 bit grayscale transparent color.
- /// This represents any color in a 16 bit grayscale encoded png that should be transparent.
+ /// Gets or sets the color table, if any.
///
- public L16? TransparentL16 { get; set; }
+ public ReadOnlyMemory? ColorTable { get; set; }
///
- /// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded.
+ /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded.
///
- public bool HasTransparency { get; set; }
+ public Color? TransparentColor { get; set; }
///
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks.
diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
index 04a23308cc..b0afd9975e 100644
--- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
+++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
@@ -18,9 +18,7 @@ public static void ProcessGrayscaleScanline(
in PngHeader header,
ReadOnlySpan scanlineSpan,
Span rowSpan,
- bool hasTrans,
- L16 luminance16Trans,
- L8 luminanceTrans)
+ Color? transparentColor)
where TPixel : unmanaged, IPixel
{
TPixel pixel = default;
@@ -28,7 +26,7 @@ public static void ProcessGrayscaleScanline(
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 @@ public static void ProcessGrayscaleScanline(
if (header.BitDepth == 16)
{
+ L16 transparent = transparentColor.Value.ToPixel();
La32 source = default;
int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance;
- source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue;
+ source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@@ -69,13 +68,13 @@ public static void ProcessGrayscaleScanline(
}
else
{
+ byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
La16 source = default;
- byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = 0; x < (uint)header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
source.L = luminance;
- source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue;
+ source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@@ -89,9 +88,7 @@ public static void ProcessInterlacedGrayscaleScanline(
Span rowSpan,
uint pixelOffset,
uint increment,
- bool hasTrans,
- L16 luminance16Trans,
- L8 luminanceTrans)
+ Color? transparentColor)
where TPixel : unmanaged, IPixel
{
TPixel pixel = default;
@@ -99,7 +96,7 @@ public static void ProcessInterlacedGrayscaleScanline(
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 @@ public static void ProcessInterlacedGrayscaleScanline(
if (header.BitDepth == 16)
{
+ L16 transparent = transparentColor.Value.ToPixel();
La32 source = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance;
- source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue;
+ source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@@ -140,13 +138,13 @@ public static void ProcessInterlacedGrayscaleScanline(
}
else
{
+ byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor);
La16 source = default;
- byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
source.L = luminance;
- source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue;
+ source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@@ -241,11 +239,10 @@ public static void ProcessPaletteScanline(
in PngHeader header,
ReadOnlySpan scanlineSpan,
Span rowSpan,
- ReadOnlySpan palette,
- byte[] paletteAlpha)
+ ReadOnlyMemory? palette)
where TPixel : unmanaged, IPixel
{
- if (palette.IsEmpty)
+ if (palette is null)
{
PngThrowHelper.ThrowMissingPalette();
}
@@ -253,36 +250,13 @@ public static void ProcessPaletteScanline(
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette);
- ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
+ ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
- if (paletteAlpha?.Length > 0)
+ for (nuint x = 0; x < (uint)header.Width; x++)
{
- // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
- // channel and we should try to read it.
- Rgba32 rgba = default;
- ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha);
-
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- uint index = Unsafe.Add(ref scanlineSpanRef, x);
- rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
- rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
-
- pixel.FromRgba32(rgba);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- for (nuint x = 0; x < (uint)header.Width; x++)
- {
- int index = Unsafe.Add(ref scanlineSpanRef, x);
- Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index);
-
- pixel.FromRgb24(rgb);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
+ uint index = Unsafe.Add(ref scanlineSpanRef, x);
+ pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
+ Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
@@ -292,42 +266,24 @@ public static void ProcessInterlacedPaletteScanline(
Span rowSpan,
uint pixelOffset,
uint increment,
- ReadOnlySpan palette,
- byte[] paletteAlpha)
+ ReadOnlyMemory? palette)
where TPixel : unmanaged, IPixel
{
+ if (palette is null)
+ {
+ PngThrowHelper.ThrowMissingPalette();
+ }
+
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette);
- ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
+ ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
- if (paletteAlpha?.Length > 0)
+ for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
{
- // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
- // channel and we should try to read it.
- Rgba32 rgba = default;
- ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha);
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
- {
- uint index = Unsafe.Add(ref scanlineSpanRef, o);
- rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
- rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
-
- pixel.FromRgba32(rgba);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
- }
- else
- {
- for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
- {
- int index = Unsafe.Add(ref scanlineSpanRef, o);
- Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index);
-
- pixel.FromRgb24(rgb);
- Unsafe.Add(ref rowSpanRef, x) = pixel;
- }
+ uint index = Unsafe.Add(ref scanlineSpanRef, o);
+ pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
+ Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
@@ -338,15 +294,13 @@ public static void ProcessRgbScanline(
Span rowSpan,
int bytesPerPixel,
int bytesPerSample,
- bool hasTrans,
- Rgb48 rgb48Trans,
- Rgb24 rgb24Trans)
+ Color? transparentColor)
where TPixel : unmanaged, IPixel
{
TPixel pixel = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
- if (!hasTrans)
+ if (transparentColor is null)
{
if (header.BitDepth == 16)
{
@@ -372,6 +326,8 @@ public static void ProcessRgbScanline(
if (header.BitDepth == 16)
{
+ Rgb48 transparent = transparentColor.Value.ToPixel();
+
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
int o = 0;
@@ -382,7 +338,7 @@ public static void ProcessRgbScanline(
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 @@ public static void ProcessRgbScanline(
}
else
{
+ Rgb24 transparent = transparentColor.Value.ToPixel();
+
Rgba32 rgba32 = default;
ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan);
ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
@@ -397,7 +355,7 @@ public static void ProcessRgbScanline(
{
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 @@ public static void ProcessInterlacedRgbScanline(
uint increment,
int bytesPerPixel,
int bytesPerSample,
- bool hasTrans,
- Rgb48 rgb48Trans,
- Rgb24 rgb24Trans)
+ Color? transparentColor)
where TPixel : unmanaged, IPixel
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
+ bool hasTransparency = transparentColor is not null;
- if (header.BitDepth == 16)
+ if (transparentColor is null)
{
- if (hasTrans)
+ if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
- Rgba64 rgba64 = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{
@@ -435,24 +391,21 @@ public static void ProcessInterlacedRgbScanline(
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 @@ public static void ProcessInterlacedRgbScanline(
return;
}
- if (hasTrans)
+ if (header.BitDepth == 16)
{
- Rgba32 rgba = default;
+ Rgb48 transparent = transparentColor.Value.ToPixel();
+
+ Rgb48 rgb48 = default;
+ Rgba64 rgba64 = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{
- rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
- rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
- rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
- rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
+ rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
+ rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
+ rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
- pixel.FromRgba32(rgba);
+ rgba64.Rgb = rgb48;
+ rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
+
+ pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
- Rgb24 rgb = default;
+ Rgb24 transparent = transparentColor.Value.ToPixel();
+
+ Rgba32 rgba = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{
- rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
- rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
- rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
+ rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
+ rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
+ rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
+ rgba.A = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
- pixel.FromRgb24(rgb);
+ pixel.FromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs
index f4b2dfc08c..39c6e62e15 100644
--- a/src/ImageSharp/Memory/Buffer2D{T}.cs
+++ b/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 x elements.
///
-///
-/// Before RC1, this class might be target of API changes, use it on your own risk!
-///
/// The value type.
public sealed class Buffer2D : IDisposable
where T : struct
diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
index a231d6dee7..524153804c 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
@@ -111,7 +111,7 @@ public WuQuantizer(Configuration configuration, QuantizerOptions options)
public QuantizerOptions Options { get; }
///
- public ReadOnlyMemory Palette
+ public readonly ReadOnlyMemory Palette
{
get
{
@@ -362,7 +362,7 @@ private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpa
///
/// The source data.
/// The bounds within the source image to quantize.
- private void Build3DHistogram(Buffer2D source, Rectangle bounds)
+ private readonly void Build3DHistogram(Buffer2D source, Rectangle bounds)
{
Span momentSpan = this.momentsOwner.GetSpan();
@@ -393,7 +393,7 @@ private void Build3DHistogram(Buffer2D source, Rectangle bounds)
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
///
/// The memory allocator used for allocating buffers.
- private void Get3DMoments(MemoryAllocator allocator)
+ private readonly void Get3DMoments(MemoryAllocator allocator)
{
using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount);
using IMemoryOwner area = allocator.Allocate(IndexAlphaCount);
@@ -462,7 +462,7 @@ private void Get3DMoments(MemoryAllocator allocator)
///
/// The cube.
/// The .
- private double Variance(ref Box cube)
+ private readonly double Variance(ref Box cube)
{
ReadOnlySpan momentSpan = this.momentsOwner.GetSpan();
@@ -503,7 +503,7 @@ private double Variance(ref Box cube)
/// The cutting point.
/// The whole moment.
/// The .
- 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 momentSpan = this.momentsOwner.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan);
@@ -634,7 +634,7 @@ private bool Cut(ref Box set1, ref Box set2)
///
/// The cube.
/// A label.
- private void Mark(ref Box cube, byte label)
+ private readonly void Mark(ref Box cube, byte label)
{
Span tagSpan = this.tagsOwner.GetSpan();
diff --git a/tests/ImageSharp.Tests/Color/ColorTests.cs b/tests/ImageSharp.Tests/Color/ColorTests.cs
index 85e6eba78c..f7e2092176 100644
--- a/tests/ImageSharp.Tests/Color/ColorTests.cs
+++ b/tests/ImageSharp.Tests/Color/ColorTests.cs
@@ -18,25 +18,42 @@ public void WithAlpha()
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());
+ c2 = Color.FromPixel(c2.ToPixel());
+ }
+
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());
+ c2 = Color.FromPixel(c2.ToPixel());
+ c3 = Color.FromPixel(c3.ToPixel());
+ }
+
Assert.False(c1.Equals(c2));
Assert.False(c2.Equals(c3));
Assert.False(c3.Equals(c1));
@@ -47,13 +64,20 @@ public void Equality_WhenFalse()
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());
+ }
+
+ string actual = color.ToHex();
Assert.Equal(expected, actual);
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 2dfd99439a..e216832853 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -539,7 +539,8 @@ public void Issue2209_Decode_HasTransparencyIsTrue(TestImageProvider 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 void Issue2209_Identify_HasTransparencyIsTrue(string imagePath)
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
PngMetadata metadata = imageInfo.Metadata.GetPngMetadata();
- Assert.True(metadata.HasTransparency);
+ Assert.NotNull(metadata.ColorTable);
+ Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255);
}
// https://github.com/SixLabors/ImageSharp/issues/410
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index b20ec0675a..3c80cfe098 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -449,44 +449,17 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo
TestFile testFile = TestFile.Create(imagePath);
using Image input = testFile.CreateRgba32Image();
PngMetadata inMeta = input.Metadata.GetPngMetadata();
- Assert.True(inMeta.HasTransparency);
+ Assert.True(inMeta.TransparentColor.HasValue);
using MemoryStream memStream = new();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
using Image output = Image.Load(memStream);
PngMetadata outMeta = output.Metadata.GetPngMetadata();
- Assert.True(outMeta.HasTransparency);
-
- switch (pngColorType)
- {
- case PngColorType.Grayscale:
- if (pngBitDepth.Equals(PngBitDepth.Bit16))
- {
- Assert.True(outMeta.TransparentL16.HasValue);
- Assert.Equal(inMeta.TransparentL16, outMeta.TransparentL16);
- }
- else
- {
- Assert.True(outMeta.TransparentL8.HasValue);
- Assert.Equal(inMeta.TransparentL8, outMeta.TransparentL8);
- }
-
- break;
- case PngColorType.Rgb:
- if (pngBitDepth.Equals(PngBitDepth.Bit16))
- {
- Assert.True(outMeta.TransparentRgb48.HasValue);
- Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48);
- }
- else
- {
- Assert.True(outMeta.TransparentRgb24.HasValue);
- Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24);
- }
-
- break;
- }
+ Assert.True(outMeta.TransparentColor.HasValue);
+ Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor);
+ Assert.Equal(pngBitDepth, outMeta.BitDepth);
+ Assert.Equal(pngColorType, outMeta.ColorType);
}
[Theory]