Skip to content

Commit f6122df

Browse files
Merge pull request #1188 from SixLabors/bp/chunkOrder
Gamma chunk should come before palette chunk
2 parents bec0b55 + 7161aa6 commit f6122df

File tree

3 files changed

+134
-13
lines changed

3 files changed

+134
-13
lines changed

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
149149
stream.Write(PngConstants.HeaderBytes);
150150

151151
this.WriteHeaderChunk(stream);
152+
this.WriteGammaChunk(stream);
152153
this.WritePaletteChunk(stream, quantized);
153154
this.WriteTransparencyChunk(stream, pngMetadata);
154155
this.WritePhysicalChunk(stream, metadata);
155-
this.WriteGammaChunk(stream);
156156
this.WriteExifChunk(stream, metadata);
157157
this.WriteTextChunks(stream, pngMetadata);
158158
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
@@ -538,6 +538,7 @@ private void WriteHeaderChunk(Stream stream)
538538

539539
/// <summary>
540540
/// Writes the palette chunk to the stream.
541+
/// Should be written before the first IDAT chunk.
541542
/// </summary>
542543
/// <typeparam name="TPixel">The pixel format.</typeparam>
543544
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
@@ -595,6 +596,7 @@ private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>
595596

596597
/// <summary>
597598
/// Writes the physical dimension information to the stream.
599+
/// Should be written before IDAT chunk.
598600
/// </summary>
599601
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
600602
/// <param name="meta">The image metadata.</param>
@@ -716,6 +718,7 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
716718

717719
/// <summary>
718720
/// Writes the gamma information to the stream.
721+
/// Should be written before PLTE and IDAT chunk.
719722
/// </summary>
720723
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
721724
private void WriteGammaChunk(Stream stream)
@@ -733,6 +736,7 @@ private void WriteGammaChunk(Stream stream)
733736

734737
/// <summary>
735738
/// Writes the transparency chunk to the stream.
739+
/// Should be written after PLTE and before IDAT.
736740
/// </summary>
737741
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
738742
/// <param name="pngMetadata">The image metadata.</param>

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

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
// ReSharper disable InconsistentNaming
5+
using System;
6+
using System.Buffers.Binary;
57
using System.IO;
68
using System.Linq;
79

@@ -18,6 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
1820
{
1921
public class PngEncoderTests
2022
{
23+
private static PngEncoder PngEncoder => new PngEncoder();
24+
2125
public static readonly TheoryData<string, PngBitDepth> PngBitDepthFiles =
2226
new TheoryData<string, PngBitDepth>
2327
{
@@ -234,8 +238,7 @@ public void InfersColorTypeAndBitDepth<TPixel>(TestImageProvider<TPixel> provide
234238
{
235239
using (Stream stream = new MemoryStream())
236240
{
237-
var encoder = new PngEncoder();
238-
encoder.Encode(provider.GetImage(), stream);
241+
PngEncoder.Encode(provider.GetImage(), stream);
239242

240243
stream.Seek(0, SeekOrigin.Begin);
241244

@@ -281,7 +284,7 @@ public void WritesFileMarker<TPixel>(TestImageProvider<TPixel> provider)
281284
using (Image<TPixel> image = provider.GetImage())
282285
using (var ms = new MemoryStream())
283286
{
284-
image.Save(ms, new PngEncoder());
287+
image.Save(ms, PngEncoder);
285288

286289
byte[] data = ms.ToArray().Take(8).ToArray();
287290
byte[] expected =
@@ -304,14 +307,12 @@ public void WritesFileMarker<TPixel>(TestImageProvider<TPixel> provider)
304307
[MemberData(nameof(RatioFiles))]
305308
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
306309
{
307-
var options = new PngEncoder();
308-
309310
var testFile = TestFile.Create(imagePath);
310311
using (Image<Rgba32> input = testFile.CreateRgba32Image())
311312
{
312313
using (var memStream = new MemoryStream())
313314
{
314-
input.Save(memStream, options);
315+
input.Save(memStream, PngEncoder);
315316

316317
memStream.Position = 0;
317318
using (var output = Image.Load<Rgba32>(memStream))
@@ -329,14 +330,12 @@ public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolut
329330
[MemberData(nameof(PngBitDepthFiles))]
330331
public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
331332
{
332-
var options = new PngEncoder();
333-
334333
var testFile = TestFile.Create(imagePath);
335334
using (Image<Rgba32> input = testFile.CreateRgba32Image())
336335
{
337336
using (var memStream = new MemoryStream())
338337
{
339-
input.Save(memStream, options);
338+
input.Save(memStream, PngEncoder);
340339

341340
memStream.Position = 0;
342341
using (var output = Image.Load<Rgba32>(memStream))
@@ -353,8 +352,6 @@ public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
353352
[MemberData(nameof(PngTrnsFiles))]
354353
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
355354
{
356-
var options = new PngEncoder();
357-
358355
var testFile = TestFile.Create(imagePath);
359356
using (Image<Rgba32> input = testFile.CreateRgba32Image())
360357
{
@@ -363,7 +360,7 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo
363360

364361
using (var memStream = new MemoryStream())
365362
{
366-
input.Save(memStream, options);
363+
input.Save(memStream, PngEncoder);
367364
memStream.Position = 0;
368365
using (var output = Image.Load<Rgba32>(memStream))
369366
{
@@ -404,6 +401,126 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo
404401
}
405402
}
406403

404+
[Fact]
405+
public void HeaderChunk_ComesFirst()
406+
{
407+
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
408+
using Image<Rgba32> input = testFile.CreateRgba32Image();
409+
using var memStream = new MemoryStream();
410+
input.Save(memStream, PngEncoder);
411+
memStream.Position = 0;
412+
413+
// Skip header.
414+
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
415+
BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
416+
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
417+
Assert.Equal(PngChunkType.Header, type);
418+
}
419+
420+
[Fact]
421+
public void EndChunk_IsLast()
422+
{
423+
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
424+
using Image<Rgba32> input = testFile.CreateRgba32Image();
425+
using var memStream = new MemoryStream();
426+
input.Save(memStream, PngEncoder);
427+
memStream.Position = 0;
428+
429+
// Skip header.
430+
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
431+
432+
bool endChunkFound = false;
433+
while (bytesSpan.Length > 0)
434+
{
435+
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
436+
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
437+
Assert.False(endChunkFound);
438+
if (type == PngChunkType.End)
439+
{
440+
endChunkFound = true;
441+
}
442+
443+
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
444+
}
445+
}
446+
447+
[Theory]
448+
[InlineData(PngChunkType.Gamma)]
449+
[InlineData(PngChunkType.Chroma)]
450+
[InlineData(PngChunkType.EmbeddedColorProfile)]
451+
[InlineData(PngChunkType.SignificantBits)]
452+
[InlineData(PngChunkType.StandardRgbColourSpace)]
453+
public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
454+
{
455+
var chunkType = (PngChunkType)chunkTypeObj;
456+
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
457+
using Image<Rgba32> input = testFile.CreateRgba32Image();
458+
using var memStream = new MemoryStream();
459+
input.Save(memStream, PngEncoder);
460+
memStream.Position = 0;
461+
462+
// Skip header.
463+
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
464+
465+
bool palFound = false;
466+
bool dataFound = false;
467+
while (bytesSpan.Length > 0)
468+
{
469+
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
470+
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
471+
if (chunkType == type)
472+
{
473+
Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
474+
}
475+
476+
switch (type)
477+
{
478+
case PngChunkType.Data:
479+
dataFound = true;
480+
break;
481+
case PngChunkType.Palette:
482+
palFound = true;
483+
break;
484+
}
485+
486+
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
487+
}
488+
}
489+
490+
[Theory]
491+
[InlineData(PngChunkType.Physical)]
492+
[InlineData(PngChunkType.SuggestedPalette)]
493+
public void Chunk_ComesBeforeIDat(object chunkTypeObj)
494+
{
495+
var chunkType = (PngChunkType)chunkTypeObj;
496+
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
497+
using Image<Rgba32> input = testFile.CreateRgba32Image();
498+
using var memStream = new MemoryStream();
499+
input.Save(memStream, PngEncoder);
500+
memStream.Position = 0;
501+
502+
// Skip header.
503+
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
504+
505+
bool dataFound = false;
506+
while (bytesSpan.Length > 0)
507+
{
508+
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
509+
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
510+
if (chunkType == type)
511+
{
512+
Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
513+
}
514+
515+
if (type == PngChunkType.Data)
516+
{
517+
dataFound = true;
518+
}
519+
520+
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
521+
}
522+
}
523+
407524
[Theory]
408525
[WithTestPatternImages(587, 821, PixelTypes.Rgba32)]
409526
[WithTestPatternImages(677, 683, PixelTypes.Rgba32)]
58.4 KB
Loading

0 commit comments

Comments
 (0)