mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
9 changed files with 626 additions and 140 deletions
@ -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 |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue