diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
index 741ec825d4..66117371ed 100644
--- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
@@ -57,5 +57,22 @@ internal interface IPngEncoderOptions
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
///
PngInterlaceMode? InterlaceMethod { get; }
+
+ ///
+ /// Gets a value indicating whether the metadata should be ignored when the image is being encoded.
+ /// When set to true, all ancillary chunks will be skipped.
+ ///
+ bool IgnoreMetadata { get; }
+
+ ///
+ /// Gets the chunk filter method. This allows to filter ancillary chunks.
+ ///
+ PngChunkFilter? ChunkFilter { get; }
+
+ ///
+ /// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
+ /// should be converted to transparent black, which can yield in better compression in some cases.
+ ///
+ PngTransparentColorMode TransparentColorMode { get; }
}
}
diff --git a/src/ImageSharp/Formats/Png/PngChunkFilter.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs
new file mode 100644
index 0000000000..4e8b5ab96f
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+using System;
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// Provides enumeration of available PNG optimization methods.
+ ///
+ [Flags]
+ public enum PngChunkFilter
+ {
+ ///
+ /// With the None filter, all chunks will be written.
+ ///
+ None = 0,
+
+ ///
+ /// Excludes the physical dimension information chunk from encoding.
+ ///
+ ExcludePhysicalChunk = 1 << 0,
+
+ ///
+ /// Excludes the gamma information chunk from encoding.
+ ///
+ ExcludeGammaChunk = 1 << 1,
+
+ ///
+ /// Excludes the eXIf chunk from encoding.
+ ///
+ ExcludeExifChunk = 1 << 2,
+
+ ///
+ /// Excludes the tTXt, iTXt or zTXt chunk from encoding.
+ ///
+ ExcludeTextChunks = 1 << 3,
+
+ ///
+ /// All ancillary chunks will be excluded.
+ ///
+ ExcludeAll = ~None
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index bee0215506..9b1fc80e07 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -34,14 +34,21 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
///
public IQuantizer Quantizer { get; set; }
- ///
- /// Gets or sets the transparency threshold.
- ///
+ ///
public byte Threshold { get; set; } = byte.MaxValue;
///
public PngInterlaceMode? InterlaceMethod { get; set; }
+ ///
+ public PngChunkFilter? ChunkFilter { get; set; }
+
+ ///
+ public bool IgnoreMetadata { get; set; }
+
+ ///
+ public PngTransparentColorMode TransparentColorMode { get; set; }
+
///
/// Encodes the image to the specified stream from the .
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index c150388477..6c30550c2a 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -7,7 +7,7 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.Advanced;
+
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
@@ -141,10 +141,18 @@ public void Encode(Image image, Stream stream)
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
- PngMetadata pngMetadata = metadata.GetPngMetadata();
+
+ PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
- IndexedImageFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
- this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
+ Image clonedImage = null;
+ bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear;
+ if (clearTransparency)
+ {
+ clonedImage = image.Clone();
+ ClearTransparentPixels(clonedImage);
+ }
+
+ IndexedImageFrame quantized = this.CreateQuantizedImage(image, clonedImage);
stream.Write(PngConstants.HeaderBytes);
@@ -155,11 +163,13 @@ public void Encode(Image image, Stream stream)
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
- this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
+ this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream);
+
stream.Flush();
quantized?.Dispose();
+ clonedImage?.Dispose();
}
///
@@ -180,6 +190,55 @@ public void Dispose()
this.filterBuffer = null;
}
+ ///
+ /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
+ ///
+ /// The type of the pixel.
+ /// The cloned image where the transparent pixels will be changed.
+ private static void ClearTransparentPixels(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ Rgba32 rgba32 = default;
+ for (int y = 0; y < image.Height; y++)
+ {
+ Span span = image.GetPixelRowSpan(y);
+ for (int x = 0; x < image.Width; x++)
+ {
+ span[x].ToRgba32(ref rgba32);
+
+ if (rgba32.A == 0)
+ {
+ span[x].FromRgba32(Color.Transparent);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Creates the quantized image and sets calculates and sets the bit depth.
+ ///
+ /// The type of the pixel.
+ /// The image to quantize.
+ /// Cloned image with transparent pixels are changed to black.
+ /// The quantized image.
+ private IndexedImageFrame CreateQuantizedImage(Image image, Image clonedImage)
+ where TPixel : unmanaged, IPixel
+ {
+ IndexedImageFrame quantized;
+ if (this.options.TransparentColorMode == PngTransparentColorMode.Clear)
+ {
+ quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage);
+ this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
+ }
+ else
+ {
+ quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
+ this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
+ }
+
+ return quantized;
+ }
+
/// Collects a row of grayscale pixels.
/// The pixel format.
/// The image row span.
@@ -602,6 +661,11 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame
/// The image metadata.
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{
+ if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
+ {
+ return;
+ }
+
PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size);
@@ -614,6 +678,11 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
/// The image metadata.
private void WriteExifChunk(Stream stream, ImageMetadata meta)
{
+ if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
+ {
+ return;
+ }
+
if (meta.ExifProfile is null || meta.ExifProfile.Values.Count == 0)
{
return;
@@ -631,6 +700,11 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta)
/// The image metadata.
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
+ if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
+ {
+ return;
+ }
+
const int MaxLatinCode = 255;
for (int i = 0; i < meta.TextData.Count; i++)
{
@@ -723,6 +797,11 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
/// The containing image data.
private void WriteGammaChunk(Stream stream)
{
+ if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
+ {
+ return;
+ }
+
if (this.options.Gamma > 0)
{
// 4-byte unsigned integer of gamma * 100,000.
@@ -792,7 +871,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
/// The image.
/// The quantized pixel data. Can be null.
/// The stream.
- private void WriteDataChunks(ImageFrame pixels, IndexedImageFrame quantized, Stream stream)
+ private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -890,8 +969,8 @@ private void AllocateExtBuffers()
/// The pixels.
/// The quantized pixels span.
/// The deflate stream.
- private void EncodePixels(ImageFrame pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
- where TPixel : unmanaged, IPixel
+ private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ where TPixel : unmanaged, IPixel
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = bytesPerScanline + 1;
@@ -914,7 +993,7 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrameThe type of the pixel.
/// The pixels.
/// The deflate stream.
- private void EncodeAdam7Pixels(ImageFrame pixels, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7Pixels(Image pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
int width = pixels.Width;
diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs
index 5545f2c6e9..53e6ee30f8 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs
@@ -29,6 +29,9 @@ public PngEncoderOptions(IPngEncoderOptions source)
this.Quantizer = source.Quantizer;
this.Threshold = source.Threshold;
this.InterlaceMethod = source.InterlaceMethod;
+ this.ChunkFilter = source.ChunkFilter;
+ this.IgnoreMetadata = source.IgnoreMetadata;
+ this.TransparentColorMode = source.TransparentColorMode;
}
///
@@ -57,5 +60,14 @@ public PngEncoderOptions(IPngEncoderOptions source)
///
public PngInterlaceMode? InterlaceMethod { get; set; }
+
+ ///
+ public PngChunkFilter? ChunkFilter { get; set; }
+
+ ///
+ public bool IgnoreMetadata { get; set; }
+
+ ///
+ public PngTransparentColorMode TransparentColorMode { get; set; }
}
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
index 33456eec51..d0f708e93f 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
@@ -40,6 +40,11 @@ public static void AdjustOptions(
use16Bit = options.BitDepth == PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit);
+ if (options.IgnoreMetadata)
+ {
+ options.ChunkFilter = PngChunkFilter.ExcludeAll;
+ }
+
// Ensure we are not allowing impossible combinations.
if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value))
{
@@ -89,11 +94,9 @@ public static IndexedImageFrame CreateQuantizedFrame(
///
/// The type of the pixel.
/// The options.
- /// The image.
/// The quantized frame.
public static byte CalculateBitDepth(
PngEncoderOptions options,
- Image image,
IndexedImageFrame quantizedFrame)
where TPixel : unmanaged, IPixel
{
diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
new file mode 100644
index 0000000000..63967c153f
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// Enum indicating how the transparency should be handled on encoding.
+ ///
+ public enum PngTransparentColorMode
+ {
+ ///
+ /// The transparency will be kept as is.
+ ///
+ Preserve = 0,
+
+ ///
+ /// Converts fully transparent pixels that may contain R, G, B values which are not 0,
+ /// to transparent black, which can yield in better compression in some cases.
+ ///
+ Clear = 1,
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
new file mode 100644
index 0000000000..fa63f73e06
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
@@ -0,0 +1,328 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using Xunit;
+
+// ReSharper disable InconsistentNaming
+namespace SixLabors.ImageSharp.Tests.Formats.Png
+{
+ public partial class PngEncoderTests
+ {
+ [Fact]
+ public void HeaderChunk_ComesFirst()
+ {
+ // arrange
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, PngEncoder);
+
+ // assert
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.Equal(PngChunkType.Header, type);
+ }
+
+ [Fact]
+ public void EndChunk_IsLast()
+ {
+ // arrange
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, PngEncoder);
+
+ // assert
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ bool endChunkFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.False(endChunkFound);
+ if (type == PngChunkType.End)
+ {
+ endChunkFound = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
+ [Theory]
+ [InlineData(PngChunkType.Gamma)]
+ [InlineData(PngChunkType.Chroma)]
+ [InlineData(PngChunkType.EmbeddedColorProfile)]
+ [InlineData(PngChunkType.SignificantBits)]
+ [InlineData(PngChunkType.StandardRgbColourSpace)]
+ public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
+ {
+ // arrange
+ var chunkType = (PngChunkType)chunkTypeObj;
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, PngEncoder);
+
+ // assert
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ bool palFound = false;
+ bool dataFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ if (chunkType == type)
+ {
+ Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
+ }
+
+ switch (type)
+ {
+ case PngChunkType.Data:
+ dataFound = true;
+ break;
+ case PngChunkType.Palette:
+ palFound = true;
+ break;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
+ [Theory]
+ [InlineData(PngChunkType.Physical)]
+ [InlineData(PngChunkType.SuggestedPalette)]
+ public void Chunk_ComesBeforeIDat(object chunkTypeObj)
+ {
+ // arrange
+ var chunkType = (PngChunkType)chunkTypeObj;
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, PngEncoder);
+
+ // assert
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ bool dataFound = false;
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ if (chunkType == type)
+ {
+ Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
+ }
+
+ if (type == PngChunkType.Data)
+ {
+ dataFound = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+
+ [Fact]
+ public void IgnoreMetadata_WillExcludeAllAncillaryChunks()
+ {
+ // arrange
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 };
+ var expectedChunkTypes = new Dictionary()
+ {
+ { PngChunkType.Header, false },
+ { PngChunkType.Palette, false },
+ { PngChunkType.Data, false },
+ { PngChunkType.End, false }
+ };
+ var excludedChunkTypes = new List()
+ {
+ PngChunkType.Gamma,
+ PngChunkType.Exif,
+ PngChunkType.Physical,
+ PngChunkType.Text,
+ PngChunkType.InternationalText,
+ PngChunkType.CompressedText,
+ };
+
+ // act
+ input.Save(memStream, encoder);
+
+ // assert
+ Assert.True(excludedChunkTypes.Count > 0);
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
+ if (expectedChunkTypes.ContainsKey(chunkType))
+ {
+ expectedChunkTypes[chunkType] = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+
+ // all expected chunk types should have been seen at least once.
+ foreach (PngChunkType chunkType in expectedChunkTypes.Keys)
+ {
+ Assert.True(expectedChunkTypes[chunkType], $"We expect {chunkType} chunk to be present at least once");
+ }
+ }
+
+ [Theory]
+ [InlineData(PngChunkFilter.ExcludeGammaChunk)]
+ [InlineData(PngChunkFilter.ExcludeExifChunk)]
+ [InlineData(PngChunkFilter.ExcludePhysicalChunk)]
+ [InlineData(PngChunkFilter.ExcludeTextChunks)]
+ [InlineData(PngChunkFilter.ExcludeAll)]
+ public void ExcludeFilter_Works(object filterObj)
+ {
+ // arrange
+ var chunkFilter = (PngChunkFilter)filterObj;
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
+ var expectedChunkTypes = new Dictionary()
+ {
+ { PngChunkType.Header, false },
+ { PngChunkType.Gamma, false },
+ { PngChunkType.Palette, false },
+ { PngChunkType.InternationalText, false },
+ { PngChunkType.Text, false },
+ { PngChunkType.CompressedText, false },
+ { PngChunkType.Exif, false },
+ { PngChunkType.Physical, false },
+ { PngChunkType.Data, false },
+ { PngChunkType.End, false }
+ };
+ var excludedChunkTypes = new List();
+ switch (chunkFilter)
+ {
+ case PngChunkFilter.ExcludeGammaChunk:
+ excludedChunkTypes.Add(PngChunkType.Gamma);
+ expectedChunkTypes.Remove(PngChunkType.Gamma);
+ break;
+ case PngChunkFilter.ExcludeExifChunk:
+ excludedChunkTypes.Add(PngChunkType.Exif);
+ expectedChunkTypes.Remove(PngChunkType.Exif);
+ break;
+ case PngChunkFilter.ExcludePhysicalChunk:
+ excludedChunkTypes.Add(PngChunkType.Physical);
+ expectedChunkTypes.Remove(PngChunkType.Physical);
+ break;
+ case PngChunkFilter.ExcludeTextChunks:
+ excludedChunkTypes.Add(PngChunkType.Text);
+ excludedChunkTypes.Add(PngChunkType.InternationalText);
+ excludedChunkTypes.Add(PngChunkType.CompressedText);
+ expectedChunkTypes.Remove(PngChunkType.Text);
+ expectedChunkTypes.Remove(PngChunkType.InternationalText);
+ expectedChunkTypes.Remove(PngChunkType.CompressedText);
+ break;
+ case PngChunkFilter.ExcludeAll:
+ excludedChunkTypes.Add(PngChunkType.Gamma);
+ excludedChunkTypes.Add(PngChunkType.Exif);
+ excludedChunkTypes.Add(PngChunkType.Physical);
+ excludedChunkTypes.Add(PngChunkType.Text);
+ excludedChunkTypes.Add(PngChunkType.InternationalText);
+ excludedChunkTypes.Add(PngChunkType.CompressedText);
+ expectedChunkTypes.Remove(PngChunkType.Gamma);
+ expectedChunkTypes.Remove(PngChunkType.Exif);
+ expectedChunkTypes.Remove(PngChunkType.Physical);
+ expectedChunkTypes.Remove(PngChunkType.Text);
+ expectedChunkTypes.Remove(PngChunkType.InternationalText);
+ expectedChunkTypes.Remove(PngChunkType.CompressedText);
+ break;
+ }
+
+ // act
+ input.Save(memStream, encoder);
+
+ // assert
+ Assert.True(excludedChunkTypes.Count > 0);
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
+ if (expectedChunkTypes.ContainsKey(chunkType))
+ {
+ expectedChunkTypes[chunkType] = true;
+ }
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+
+ // all expected chunk types should have been seen at least once.
+ foreach (PngChunkType chunkType in expectedChunkTypes.Keys)
+ {
+ Assert.True(expectedChunkTypes[chunkType], $"We expect {chunkType} chunk to be present at least once");
+ }
+ }
+
+ [Fact]
+ public void ExcludeFilter_WithNone_DoesNotExcludeChunks()
+ {
+ // arrange
+ var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
+ using Image input = testFile.CreateRgba32Image();
+ using var memStream = new MemoryStream();
+ var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
+ var expectedChunkTypes = new List()
+ {
+ PngChunkType.Header,
+ PngChunkType.Gamma,
+ PngChunkType.Palette,
+ PngChunkType.InternationalText,
+ PngChunkType.Text,
+ PngChunkType.CompressedText,
+ PngChunkType.Exif,
+ PngChunkType.Physical,
+ PngChunkType.Data,
+ PngChunkType.End,
+ };
+
+ // act
+ input.Save(memStream, encoder);
+ memStream.Position = 0;
+ Span bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
+ while (bytesSpan.Length > 0)
+ {
+ int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
+ var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
+ Assert.True(expectedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been present");
+
+ bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 7828134a75..4f2490f9a6 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -2,8 +2,6 @@
// Licensed under the GNU Affero General Public License, Version 3.
// ReSharper disable InconsistentNaming
-using System;
-using System.Buffers.Binary;
using System.IO;
using System.Linq;
@@ -18,7 +16,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
- public class PngEncoderTests
+ public partial class PngEncoderTests
{
private static PngEncoder PngEncoder => new PngEncoder();
@@ -215,6 +213,40 @@ public void WorksWithAllBitDepths(TestImageProvider provider, Pn
}
}
+ [Theory]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
+ [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)]
+ public void WorksWithAllBitDepthsAndExcludeAllFilter(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
+ where TPixel : unmanaged, IPixel
+ {
+ foreach (PngInterlaceMode interlaceMode in InterlaceMode)
+ {
+ TestPngEncoderCore(
+ provider,
+ pngColorType,
+ PngFilterMethod.Adaptive,
+ pngBitDepth,
+ interlaceMode,
+ appendPngColorType: true,
+ appendPixelType: true,
+ appendPngBitDepth: true,
+ optimizeMethod: PngChunkFilter.ExcludeAll);
+ }
+ }
+
[Theory]
[WithBlankImages(1, 1, PixelTypes.A8, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
[WithBlankImages(1, 1, PixelTypes.Argb32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
@@ -358,6 +390,66 @@ public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
}
}
+ [Theory]
+ [InlineData(PngColorType.Palette)]
+ [InlineData(PngColorType.RgbWithAlpha)]
+ [InlineData(PngColorType.GrayscaleWithAlpha)]
+ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType)
+ {
+ // arrange
+ var image = new Image(50, 50);
+ var encoder = new PngEncoder()
+ {
+ TransparentColorMode = PngTransparentColorMode.Clear,
+ ColorType = colorType
+ };
+ Rgba32 rgba32 = Color.Blue;
+ for (int y = 0; y < image.Height; y++)
+ {
+ System.Span rowSpan = image.GetPixelRowSpan(y);
+
+ // Half of the test image should be transparent.
+ if (y > 25)
+ {
+ rgba32.A = 0;
+ }
+
+ for (int x = 0; x < image.Width; x++)
+ {
+ rowSpan[x].FromRgba32(rgba32);
+ }
+ }
+
+ // act
+ using var memStream = new MemoryStream();
+ image.Save(memStream, encoder);
+
+ // assert
+ memStream.Position = 0;
+ using var actual = Image.Load(memStream);
+ Rgba32 expectedColor = Color.Blue;
+ if (colorType == PngColorType.Grayscale || colorType == PngColorType.GrayscaleWithAlpha)
+ {
+ var luminance = ImageMaths.Get8BitBT709Luminance(expectedColor.R, expectedColor.G, expectedColor.B);
+ expectedColor = new Rgba32(luminance, luminance, luminance);
+ }
+
+ for (int y = 0; y < actual.Height; y++)
+ {
+ System.Span rowSpan = actual.GetPixelRowSpan(y);
+
+ if (y > 25)
+ {
+ expectedColor = Color.Transparent;
+ }
+
+ for (int x = 0; x < actual.Width; x++)
+ {
+ Assert.Equal(expectedColor, rowSpan[x]);
+ }
+ }
+ }
+
[Theory]
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
@@ -411,126 +503,6 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo
}
}
- [Fact]
- public void HeaderChunk_ComesFirst()
- {
- var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
- using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
- input.Save(memStream, PngEncoder);
- memStream.Position = 0;
-
- // Skip header.
- Span bytesSpan = memStream.ToArray().AsSpan(8);
- BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
- var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
- Assert.Equal(PngChunkType.Header, type);
- }
-
- [Fact]
- public void EndChunk_IsLast()
- {
- var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
- using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
- input.Save(memStream, PngEncoder);
- memStream.Position = 0;
-
- // Skip header.
- Span bytesSpan = memStream.ToArray().AsSpan(8);
-
- bool endChunkFound = false;
- while (bytesSpan.Length > 0)
- {
- int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
- var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
- Assert.False(endChunkFound);
- if (type == PngChunkType.End)
- {
- endChunkFound = true;
- }
-
- bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
- }
- }
-
- [Theory]
- [InlineData(PngChunkType.Gamma)]
- [InlineData(PngChunkType.Chroma)]
- [InlineData(PngChunkType.EmbeddedColorProfile)]
- [InlineData(PngChunkType.SignificantBits)]
- [InlineData(PngChunkType.StandardRgbColourSpace)]
- public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
- {
- var chunkType = (PngChunkType)chunkTypeObj;
- var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
- using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
- input.Save(memStream, PngEncoder);
- memStream.Position = 0;
-
- // Skip header.
- Span bytesSpan = memStream.ToArray().AsSpan(8);
-
- bool palFound = false;
- bool dataFound = false;
- while (bytesSpan.Length > 0)
- {
- int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
- var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
- if (chunkType == type)
- {
- Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
- }
-
- switch (type)
- {
- case PngChunkType.Data:
- dataFound = true;
- break;
- case PngChunkType.Palette:
- palFound = true;
- break;
- }
-
- bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
- }
- }
-
- [Theory]
- [InlineData(PngChunkType.Physical)]
- [InlineData(PngChunkType.SuggestedPalette)]
- public void Chunk_ComesBeforeIDat(object chunkTypeObj)
- {
- var chunkType = (PngChunkType)chunkTypeObj;
- var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
- using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
- input.Save(memStream, PngEncoder);
- memStream.Position = 0;
-
- // Skip header.
- Span bytesSpan = memStream.ToArray().AsSpan(8);
-
- bool dataFound = false;
- while (bytesSpan.Length > 0)
- {
- int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
- var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
- if (chunkType == type)
- {
- Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
- }
-
- if (type == PngChunkType.Data)
- {
- dataFound = true;
- }
-
- bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
- }
- }
-
[Theory]
[WithTestPatternImages(587, 821, PixelTypes.Rgba32)]
[WithTestPatternImages(677, 683, PixelTypes.Rgba32)]
@@ -564,8 +536,9 @@ private static void TestPngEncoderCore(
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false,
- bool appendPngBitDepth = false)
- where TPixel : unmanaged, IPixel
+ bool appendPngBitDepth = false,
+ PngChunkFilter optimizeMethod = PngChunkFilter.None)
+ where TPixel : unmanaged, IPixel
{
using (Image image = provider.GetImage())
{
@@ -576,7 +549,8 @@ private static void TestPngEncoderCore(
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }),
- InterlaceMethod = interlaceMode
+ InterlaceMethod = interlaceMode,
+ ChunkFilter = optimizeMethod,
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;