Browse Source

Merge branch 'master' into js/fast-hash

pull/1574/head
James Jackson-South 6 years ago
committed by GitHub
parent
commit
88354cb0ee
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  2. 44
      src/ImageSharp/Formats/Png/PngChunkFilter.cs
  3. 13
      src/ImageSharp/Formats/Png/PngEncoder.cs
  4. 97
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  5. 12
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  6. 7
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  7. 22
      src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
  8. 328
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  9. 226
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

17
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -57,5 +57,22 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
PngInterlaceMode? InterlaceMethod { get; }
/// <summary>
/// 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.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the chunk filter method. This allows to filter ancillary chunks.
/// </summary>
PngChunkFilter? ChunkFilter { get; }
/// <summary>
/// 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.
/// </summary>
PngTransparentColorMode TransparentColorMode { get; }
}
}

44
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
{
/// <summary>
/// Provides enumeration of available PNG optimization methods.
/// </summary>
[Flags]
public enum PngChunkFilter
{
/// <summary>
/// With the None filter, all chunks will be written.
/// </summary>
None = 0,
/// <summary>
/// Excludes the physical dimension information chunk from encoding.
/// </summary>
ExcludePhysicalChunk = 1 << 0,
/// <summary>
/// Excludes the gamma information chunk from encoding.
/// </summary>
ExcludeGammaChunk = 1 << 1,
/// <summary>
/// Excludes the eXIf chunk from encoding.
/// </summary>
ExcludeExifChunk = 1 << 2,
/// <summary>
/// Excludes the tTXt, iTXt or zTXt chunk from encoding.
/// </summary>
ExcludeTextChunks = 1 << 3,
/// <summary>
/// All ancillary chunks will be excluded.
/// </summary>
ExcludeAll = ~None
}
}

13
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -34,14 +34,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
/// <inheritdoc/>
public byte Threshold { get; set; } = byte.MaxValue;
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>

97
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -7,7 +7,7 @@ using System.Buffers.Binary;
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;
@ -136,10 +136,18 @@ namespace SixLabors.ImageSharp.Formats.Png
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata();
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
IndexedImageFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear;
if (clearTransparency)
{
clonedImage = image.Clone();
ClearTransparentPixels(clonedImage);
}
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage);
stream.Write(PngConstants.HeaderBytes);
@ -150,11 +158,13 @@ namespace SixLabors.ImageSharp.Formats.Png
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();
}
/// <inheritdoc />
@ -175,6 +185,55 @@ namespace SixLabors.ImageSharp.Formats.Png
this.filterBuffer = null;
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba32 = default;
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> 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);
}
}
}
}
/// <summary>
/// Creates the quantized image and sets calculates and sets the bit depth.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImage<TPixel>(Image<TPixel> image, Image<TPixel> clonedImage)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> 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;
}
/// <summary>Collects a row of grayscale pixels.</summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The image row span.</param>
@ -597,6 +656,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
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);
@ -609,6 +673,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
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;
@ -626,6 +695,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
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++)
{
@ -718,6 +792,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
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.
@ -787,7 +866,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] buffer;
@ -885,8 +964,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = bytesPerScanline + 1;
@ -909,7 +988,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="pixels">The pixels.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> pixels, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;

12
src/ImageSharp/Formats/Png/PngEncoderOptions.cs

@ -29,6 +29,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.Quantizer = source.Quantizer;
this.Threshold = source.Threshold;
this.InterlaceMethod = source.InterlaceMethod;
this.ChunkFilter = source.ChunkFilter;
this.IgnoreMetadata = source.IgnoreMetadata;
this.TransparentColorMode = source.TransparentColorMode;
}
/// <inheritdoc/>
@ -57,5 +60,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
}
}

7
src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

@ -40,6 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Png
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 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
/// <param name="quantizedFrame">The quantized frame.</param>
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{

22
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
{
/// <summary>
/// Enum indicating how the transparency should be handled on encoding.
/// </summary>
public enum PngTransparentColorMode
{
/// <summary>
/// The transparency will be kept as is.
/// </summary>
Preserve = 0,
/// <summary>
/// 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.
/// </summary>
Clear = 1,
}
}

328
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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
{
{ PngChunkType.Header, false },
{ PngChunkType.Palette, false },
{ PngChunkType.Data, false },
{ PngChunkType.End, false }
};
var excludedChunkTypes = new List<PngChunkType>()
{
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<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
{
{ 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<PngChunkType>();
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<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
var expectedChunkTypes = new List<PngChunkType>()
{
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<byte> 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);
}
}
}
}

226
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 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngEncoderTests
public partial class PngEncoderTests
{
private static PngEncoder PngEncoder => new PngEncoder();
@ -215,6 +213,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[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<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : unmanaged, IPixel<TPixel>
{
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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Theory]
[InlineData(PngColorType.Palette)]
[InlineData(PngColorType.RgbWithAlpha)]
[InlineData(PngColorType.GrayscaleWithAlpha)]
public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType)
{
// arrange
var image = new Image<Rgba32>(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<Rgba32> 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<Rgba32>(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<Rgba32> 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Fact]
public void HeaderChunk_ComesFirst()
{
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> 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<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false,
bool appendPngBitDepth = false)
where TPixel : unmanaged, IPixel<TPixel>
bool appendPngBitDepth = false,
PngChunkFilter optimizeMethod = PngChunkFilter.None)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
@ -576,7 +549,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }),
InterlaceMethod = interlaceMode
InterlaceMethod = interlaceMode,
ChunkFilter = optimizeMethod,
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;

Loading…
Cancel
Save