Skip to content

Commit

Permalink
Merge pull request #2485 from SixLabors/js/png-pallete
Browse files Browse the repository at this point in the history
Expose and conserve the color palette for indexed png images.
  • Loading branch information
JimBobSquarePants committed Oct 3, 2023
2 parents 63d1d2c + 31b591a commit e905b0a
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 247 deletions.
12 changes: 11 additions & 1 deletion src/ImageSharp/Color/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,17 @@ public Color WithAlpha(float alpha)
/// </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();
Expand Down
8 changes: 4 additions & 4 deletions src/ImageSharp/Formats/Png/PngDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);

Expand Down
103 changes: 62 additions & 41 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,20 @@ public Image<TPixel> Decode<TPixel>(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());
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -766,9 +771,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
this.header,
scanlineSpan,
rowSpan,
pngMetadata.HasTransparency,
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
pngMetadata.TransparentColor);

break;

Expand All @@ -787,8 +790,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
this.header,
scanlineSpan,
rowSpan,
this.palette,
this.paletteAlpha);
pngMetadata.ColorTable);

break;

Expand All @@ -800,9 +802,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
rowSpan,
this.bytesPerPixel,
this.bytesPerSample,
pngMetadata.HasTransparency,
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
pngMetadata.TransparentColor);

break;

Expand Down Expand Up @@ -860,9 +860,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
rowSpan,
(uint)pixelOffset,
(uint)increment,
pngMetadata.HasTransparency,
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
pngMetadata.TransparentColor);

break;

Expand All @@ -885,8 +883,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
rowSpan,
(uint)pixelOffset,
(uint)increment,
this.palette,
this.paletteAlpha);
pngMetadata.ColorTable);

break;

Expand All @@ -899,9 +896,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
(uint)increment,
this.bytesPerPixel,
this.bytesPerSample,
pngMetadata.HasTransparency,
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
pngMetadata.TransparentColor);

break;

Expand All @@ -924,10 +919,44 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
}
}

/// <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)
{
Expand All @@ -941,16 +970,14 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> 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)
Expand All @@ -959,20 +986,14 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> 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;
}
}

/// <summary>
Expand Down Expand Up @@ -1461,7 +1482,7 @@ private bool TryReadChunk(Span<byte> 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);

Expand Down
11 changes: 11 additions & 0 deletions src/ImageSharp/Formats/Png/PngEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#nullable disable

using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Processing.Processors.Quantization;

namespace SixLabors.ImageSharp.Formats.Png;

Expand All @@ -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.
Expand Down
40 changes: 27 additions & 13 deletions src/ImageSharp/Formats/Png/PngEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -889,27 +889,27 @@ private void WriteGammaChunk(Stream stream)
/// <param name="pngMetadata">The image metadata.</param>
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
if (!pngMetadata.HasTransparency)
if (pngMetadata.TransparentColor is null)
{
return;
}

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;
Expand All @@ -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<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);
}
}
Expand Down Expand Up @@ -1175,7 +1177,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span<byte> 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)
{
Expand Down Expand Up @@ -1290,8 +1292,20 @@ private void SwapScanlineBuffers()
}

// 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());
Expand Down

0 comments on commit e905b0a

Please sign in to comment.