Skip to content

Commit e905b0a

Browse files
Merge pull request #2485 from SixLabors/js/png-pallete
Expose and conserve the color palette for indexed png images.
2 parents 63d1d2c + 31b591a commit e905b0a

File tree

12 files changed

+240
-247
lines changed

12 files changed

+240
-247
lines changed

src/ImageSharp/Color/Color.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,17 @@ public Color WithAlpha(float alpha)
251251
/// </summary>
252252
/// <returns>A hexadecimal string representation of the value.</returns>
253253
[MethodImpl(InliningOptions.ShortMethod)]
254-
public string ToHex() => this.data.ToRgba32().ToHex();
254+
public string ToHex()
255+
{
256+
if (this.boxedHighPrecisionPixel is not null)
257+
{
258+
Rgba32 rgba = default;
259+
this.boxedHighPrecisionPixel.ToRgba32(ref rgba);
260+
return rgba.ToHex();
261+
}
262+
263+
return this.data.ToRgba32().ToHex();
264+
}
255265

256266
/// <inheritdoc />
257267
public override string ToString() => this.ToHex();

src/ImageSharp/Formats/Png/PngDecoder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,24 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat
6161
case PngColorType.Grayscale:
6262
if (bits == PngBitDepth.Bit16)
6363
{
64-
return !meta.HasTransparency
64+
return !meta.TransparentColor.HasValue
6565
? this.Decode<L16>(options, stream, cancellationToken)
6666
: this.Decode<La32>(options, stream, cancellationToken);
6767
}
6868

69-
return !meta.HasTransparency
69+
return !meta.TransparentColor.HasValue
7070
? this.Decode<L8>(options, stream, cancellationToken)
7171
: this.Decode<La16>(options, stream, cancellationToken);
7272

7373
case PngColorType.Rgb:
7474
if (bits == PngBitDepth.Bit16)
7575
{
76-
return !meta.HasTransparency
76+
return !meta.TransparentColor.HasValue
7777
? this.Decode<Rgb48>(options, stream, cancellationToken)
7878
: this.Decode<Rgba64>(options, stream, cancellationToken);
7979
}
8080

81-
return !meta.HasTransparency
81+
return !meta.TransparentColor.HasValue
8282
? this.Decode<Rgb24>(options, stream, cancellationToken)
8383
: this.Decode<Rgba32>(options, stream, cancellationToken);
8484

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,20 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
172172
if (image is null)
173173
{
174174
this.InitializeImage(metadata, out image);
175+
176+
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
177+
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
175178
}
176179

177180
this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken);
178181

179182
break;
180183
case PngChunkType.Palette:
181-
byte[] pal = new byte[chunk.Length];
182-
chunk.Data.GetSpan().CopyTo(pal);
183-
this.palette = pal;
184+
this.palette = chunk.Data.GetSpan().ToArray();
184185
break;
185186
case PngChunkType.Transparency:
186-
byte[] alpha = new byte[chunk.Length];
187-
chunk.Data.GetSpan().CopyTo(alpha);
188-
this.paletteAlpha = alpha;
189-
this.AssignTransparentMarkers(alpha, pngMetadata);
187+
this.paletteAlpha = chunk.Data.GetSpan().ToArray();
188+
this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
190189
break;
191190
case PngChunkType.Text:
192191
this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
@@ -292,12 +291,15 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
292291

293292
this.SkipChunkDataAndCrc(chunk);
294293
break;
294+
case PngChunkType.Palette:
295+
this.palette = chunk.Data.GetSpan().ToArray();
296+
break;
297+
295298
case PngChunkType.Transparency:
296-
byte[] alpha = new byte[chunk.Length];
297-
chunk.Data.GetSpan().CopyTo(alpha);
298-
this.paletteAlpha = alpha;
299-
this.AssignTransparentMarkers(alpha, pngMetadata);
299+
this.paletteAlpha = chunk.Data.GetSpan().ToArray();
300+
this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
300301

302+
// Spec says tRNS must be after PLTE so safe to exit.
301303
if (this.colorMetadataOnly)
302304
{
303305
goto EOF;
@@ -370,6 +372,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
370372
PngThrowHelper.ThrowNoHeader();
371373
}
372374

375+
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
376+
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
377+
373378
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
374379
}
375380
finally
@@ -766,9 +771,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
766771
this.header,
767772
scanlineSpan,
768773
rowSpan,
769-
pngMetadata.HasTransparency,
770-
pngMetadata.TransparentL16.GetValueOrDefault(),
771-
pngMetadata.TransparentL8.GetValueOrDefault());
774+
pngMetadata.TransparentColor);
772775

773776
break;
774777

@@ -787,8 +790,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
787790
this.header,
788791
scanlineSpan,
789792
rowSpan,
790-
this.palette,
791-
this.paletteAlpha);
793+
pngMetadata.ColorTable);
792794

793795
break;
794796

@@ -800,9 +802,7 @@ private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScan
800802
rowSpan,
801803
this.bytesPerPixel,
802804
this.bytesPerSample,
803-
pngMetadata.HasTransparency,
804-
pngMetadata.TransparentRgb48.GetValueOrDefault(),
805-
pngMetadata.TransparentRgb24.GetValueOrDefault());
805+
pngMetadata.TransparentColor);
806806

807807
break;
808808

@@ -860,9 +860,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
860860
rowSpan,
861861
(uint)pixelOffset,
862862
(uint)increment,
863-
pngMetadata.HasTransparency,
864-
pngMetadata.TransparentL16.GetValueOrDefault(),
865-
pngMetadata.TransparentL8.GetValueOrDefault());
863+
pngMetadata.TransparentColor);
866864

867865
break;
868866

@@ -885,8 +883,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
885883
rowSpan,
886884
(uint)pixelOffset,
887885
(uint)increment,
888-
this.palette,
889-
this.paletteAlpha);
886+
pngMetadata.ColorTable);
890887

891888
break;
892889

@@ -899,9 +896,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
899896
(uint)increment,
900897
this.bytesPerPixel,
901898
this.bytesPerSample,
902-
pngMetadata.HasTransparency,
903-
pngMetadata.TransparentRgb48.GetValueOrDefault(),
904-
pngMetadata.TransparentRgb24.GetValueOrDefault());
899+
pngMetadata.TransparentColor);
905900

906901
break;
907902

@@ -924,10 +919,44 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
924919
}
925920
}
926921

922+
/// <summary>
923+
/// Decodes and assigns the color palette to the metadata
924+
/// </summary>
925+
/// <param name="palette">The palette buffer.</param>
926+
/// <param name="alpha">The alpha palette buffer.</param>
927+
/// <param name="pngMetadata">The png metadata.</param>
928+
private static void AssignColorPalette(ReadOnlySpan<byte> palette, ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
929+
{
930+
if (palette.Length == 0)
931+
{
932+
return;
933+
}
934+
935+
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Rgb24>()];
936+
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(palette);
937+
for (int i = 0; i < colorTable.Length; i++)
938+
{
939+
colorTable[i] = new Color(rgbTable[i]);
940+
}
941+
942+
if (alpha.Length > 0)
943+
{
944+
// The alpha chunk may contain as many transparency entries as there are palette entries
945+
// (more than that would not make any sense) or as few as one.
946+
for (int i = 0; i < alpha.Length; i++)
947+
{
948+
ref Color color = ref colorTable[i];
949+
color = color.WithAlpha(alpha[i] / 255F);
950+
}
951+
}
952+
953+
pngMetadata.ColorTable = colorTable;
954+
}
955+
927956
/// <summary>
928957
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images.
929958
/// </summary>
930-
/// <param name="alpha">The alpha tRNS array.</param>
959+
/// <param name="alpha">The alpha tRNS buffer.</param>
931960
/// <param name="pngMetadata">The png metadata.</param>
932961
private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
933962
{
@@ -941,16 +970,14 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngM
941970
ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2));
942971
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
943972

944-
pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc);
945-
pngMetadata.HasTransparency = true;
973+
pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc));
946974
return;
947975
}
948976

949977
byte r = ReadByteLittleEndian(alpha, 0);
950978
byte g = ReadByteLittleEndian(alpha, 2);
951979
byte b = ReadByteLittleEndian(alpha, 4);
952-
pngMetadata.TransparentRgb24 = new Rgb24(r, g, b);
953-
pngMetadata.HasTransparency = true;
980+
pngMetadata.TransparentColor = new(new Rgb24(r, g, b));
954981
}
955982
}
956983
else if (this.pngColorType == PngColorType.Grayscale)
@@ -959,20 +986,14 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngM
959986
{
960987
if (this.header.BitDepth == 16)
961988
{
962-
pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]));
989+
pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])));
963990
}
964991
else
965992
{
966-
pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0));
993+
pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0)));
967994
}
968-
969-
pngMetadata.HasTransparency = true;
970995
}
971996
}
972-
else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0)
973-
{
974-
pngMetadata.HasTransparency = true;
975-
}
976997
}
977998

978999
/// <summary>
@@ -1461,7 +1482,7 @@ private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
14611482

14621483
// If we're reading color metadata only we're only interested in the IHDR and tRNS chunks.
14631484
// We can skip all other chunk data in the stream for better performance.
1464-
if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency)
1485+
if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette)
14651486
{
14661487
chunk = new PngChunk(length, type);
14671488

src/ImageSharp/Formats/Png/PngEncoder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#nullable disable
44

55
using SixLabors.ImageSharp.Advanced;
6+
using SixLabors.ImageSharp.Processing.Processors.Quantization;
67

78
namespace SixLabors.ImageSharp.Formats.Png;
89

@@ -11,6 +12,16 @@ namespace SixLabors.ImageSharp.Formats.Png;
1112
/// </summary>
1213
public class PngEncoder : QuantizingImageEncoder
1314
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
17+
/// </summary>
18+
public PngEncoder()
19+
20+
// Hack. TODO: Investigate means to fix/optimize the Wu quantizer.
21+
// The Wu quantizer does not handle the default sampling strategy well for some larger images.
22+
// It's expensive and the results are not better than the extensive strategy.
23+
=> this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy();
24+
1425
/// <summary>
1526
/// Gets the number of bits per sample or per palette index (not per pixel).
1627
/// Not all values are allowed for all <see cref="ColorType" /> values.

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ private void WriteGammaChunk(Stream stream)
875875
// 4-byte unsigned integer of gamma * 100,000.
876876
uint gammaValue = (uint)(this.gamma * 100_000F);
877877

878-
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue);
878+
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue);
879879

880880
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
881881
}
@@ -889,27 +889,27 @@ private void WriteGammaChunk(Stream stream)
889889
/// <param name="pngMetadata">The image metadata.</param>
890890
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
891891
{
892-
if (!pngMetadata.HasTransparency)
892+
if (pngMetadata.TransparentColor is null)
893893
{
894894
return;
895895
}
896896

897897
Span<byte> alpha = this.chunkDataBuffer.Span;
898898
if (pngMetadata.ColorType == PngColorType.Rgb)
899899
{
900-
if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit)
900+
if (this.use16Bit)
901901
{
902-
Rgb48 rgb = pngMetadata.TransparentRgb48.Value;
902+
Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel<Rgb48>();
903903
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
904904
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
905905
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
906906

907907
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
908908
}
909-
else if (pngMetadata.TransparentRgb24.HasValue)
909+
else
910910
{
911911
alpha.Clear();
912-
Rgb24 rgb = pngMetadata.TransparentRgb24.Value;
912+
Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24();
913913
alpha[1] = rgb.R;
914914
alpha[3] = rgb.G;
915915
alpha[5] = rgb.B;
@@ -918,15 +918,17 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
918918
}
919919
else if (pngMetadata.ColorType == PngColorType.Grayscale)
920920
{
921-
if (pngMetadata.TransparentL16.HasValue && this.use16Bit)
921+
if (this.use16Bit)
922922
{
923-
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue);
923+
L16 l16 = pngMetadata.TransparentColor.Value.ToPixel<L16>();
924+
BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
924925
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
925926
}
926-
else if (pngMetadata.TransparentL8.HasValue)
927+
else
927928
{
929+
L8 l8 = pngMetadata.TransparentColor.Value.ToPixel<L8>();
928930
alpha.Clear();
929-
alpha[1] = pngMetadata.TransparentL8.Value.PackedValue;
931+
alpha[1] = l8.PackedValue;
930932
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
931933
}
932934
}
@@ -1175,7 +1177,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int o
11751177

11761178
stream.Write(buffer);
11771179

1178-
uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer
1180+
uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer
11791181

11801182
if (data.Length > 0 && length > 0)
11811183
{
@@ -1290,8 +1292,20 @@ private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
12901292
}
12911293

12921294
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
1293-
IQuantizer quantizer = encoder.Quantizer
1294-
?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
1295+
IQuantizer quantizer = encoder.Quantizer;
1296+
if (quantizer is null)
1297+
{
1298+
PngMetadata metadata = image.Metadata.GetPngMetadata();
1299+
if (metadata.ColorTable is not null)
1300+
{
1301+
// Use the provided palette in total. The caller is responsible for setting values.
1302+
quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
1303+
}
1304+
else
1305+
{
1306+
quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
1307+
}
1308+
}
12951309

12961310
// Create quantized frame returning the palette and set the bit depth.
12971311
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration());

0 commit comments

Comments
 (0)