Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9d62a2c
Added: ability to skip unneeded chunks for optimization mode
equinox2k Sep 19, 2019
69f01c3
Update: forgot to implement Optimized property
equinox2k Sep 19, 2019
9ee2066
Added: test + fixed optimized property
equinox2k Sep 19, 2019
8d9fbb7
Fixed property documentation + Optimized transparency
equinox2k Sep 19, 2019
81bc719
Update: expanded optimize options
equinox2k Sep 20, 2019
fc8f5e3
Update: transparent pixels to black before quantization
equinox2k Sep 20, 2019
ba08c64
Fixed tests
equinox2k Sep 20, 2019
f8c5277
Fixed transparency update
equinox2k Sep 20, 2019
6e3e4ce
Fixed formatting
equinox2k Sep 20, 2019
6259881
Merge branch 'master' into PNGOptimisation
equinox2k Sep 30, 2019
d68d7bd
Merge remote-tracking branch 'upstream/master' into PNGOptimisation
brianpopow Apr 29, 2020
6765f96
Renamed enum to PngChunkFilter and also renamed enum names
brianpopow Apr 29, 2020
f257ef2
Add tests for exclude filter
brianpopow Apr 29, 2020
f04e836
MakeTransparentBlack is now a png encoder option
brianpopow Apr 29, 2020
624591c
Refactor
brianpopow Apr 29, 2020
a86713f
Add tests for make transparent black option
brianpopow Apr 29, 2020
8ca9b97
MakeTransparentBlack option now work with all png color type
brianpopow Apr 29, 2020
c386f9d
Merge branch 'master' into PNGOptimisation
brianpopow Apr 29, 2020
200ef9f
ExcludeAll = ~None
brianpopow Apr 30, 2020
f9c2f6a
Improve exclude filter test to also check presence of expected chunks
brianpopow Apr 30, 2020
0f581ab
Remove not needed image parameter from CalculateBitDepth
brianpopow May 1, 2020
bc7eb27
Add IgnoreMetadata to the png encoder options
brianpopow May 1, 2020
ec3656f
Add PngTransparentColorBehavior enum
brianpopow May 1, 2020
749f68a
Merge branch 'master' into PNGOptimisation
brianpopow May 1, 2020
129b639
Switched Preserve to be 0 for PngTransparentColorBehavior enum
brianpopow May 1, 2020
b69d772
Rename PngTransparentColorBehavior to PngTransparentColorMode
brianpopow May 1, 2020
b817615
Merge branch 'master' into PNGOptimisation
JimBobSquarePants May 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,20 @@ internal interface IPngEncoderOptions
PngInterlaceMode? InterlaceMethod { get; }

/// <summary>
/// Gets chunk filter method.
/// 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 should be converted to black pixels.
/// 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>
bool MakeTransparentBlack { get; }
PngTransparentColorBehavior TransparentColorBehavior { get; }
}
}
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Png/PngChunkFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public enum PngChunkFilter
ExcludeTextChunks = 1 << 3,

/// <summary>
/// All possible optimizations.
/// All ancillary chunks will be excluded.
/// </summary>
ExcludeAll = ~None
}
Expand Down
17 changes: 7 additions & 10 deletions src/ImageSharp/Formats/Png/PngEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,20 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
/// <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; }

/// <summary>
/// Gets or sets the chunk filter. This can be used to exclude some ancillary chunks from being written.
/// </summary>
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }

/// <summary>
/// Gets or sets a value indicating whether fully transparent pixels should be converted to black pixels.
/// </summary>
public bool MakeTransparentBlack { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }

/// <inheritdoc/>
public PngTransparentColorBehavior TransparentColorBehavior { get; set; }

/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
Expand Down
19 changes: 10 additions & 9 deletions src/ImageSharp/Formats/Png/PngEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null;
if (this.options.MakeTransparentBlack)
bool clearTransparency = this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear;
if (clearTransparency)
{
clonedImage = image.Clone();
MakeTransparentPixelsBlack(clonedImage);
ClearTransparentPixels(clonedImage);
}

IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage);
Expand All @@ -162,7 +163,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(this.options.MakeTransparentBlack ? clonedImage : image, quantized, stream);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream);

stream.Flush();
Expand Down Expand Up @@ -190,11 +191,11 @@ public void Dispose()
}

/// <summary>
/// Makes transparent pixels black.
/// 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 MakeTransparentPixelsBlack<TPixel>(Image<TPixel> image)
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba32 = default;
Expand All @@ -207,7 +208,7 @@ private static void MakeTransparentPixelsBlack<TPixel>(Image<TPixel> image)

if (rgba32.A == 0)
{
span[x].FromRgba32(Color.Black);
span[x].FromRgba32(Color.Transparent);
}
}
}
Expand All @@ -224,15 +225,15 @@ private IndexedImageFrame<TPixel> CreateQuantizedImage<TPixel>(Image<TPixel> ima
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> quantized;
if (this.options.MakeTransparentBlack)
if (this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear)
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, clonedImage, quantized);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
}
else
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
}

return quantized;
Expand Down
12 changes: 7 additions & 5 deletions src/ImageSharp/Formats/Png/PngEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public PngEncoderOptions(IPngEncoderOptions source)
this.Threshold = source.Threshold;
this.InterlaceMethod = source.InterlaceMethod;
this.ChunkFilter = source.ChunkFilter;
this.MakeTransparentBlack = source.MakeTransparentBlack;
this.IgnoreMetadata = source.IgnoreMetadata;
this.TransparentColorBehavior = source.TransparentColorBehavior;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -60,12 +61,13 @@ public PngEncoderOptions(IPngEncoderOptions source)
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }

/// <summary>
/// Gets or sets a the optimize method.
/// </summary>
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }

/// <inheritdoc/>
public bool MakeTransparentBlack { get; set; }
public bool IgnoreMetadata { get; set; }

/// <inheritdoc/>
public PngTransparentColorBehavior TransparentColorBehavior { get; set; }
}
}
7 changes: 5 additions & 2 deletions src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public static void AdjustOptions<TPixel>(
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))
{
Expand Down Expand Up @@ -89,11 +94,9 @@ public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
/// </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>
{
Expand Down
22 changes: 22 additions & 0 deletions src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs
Original file line number Diff line number Diff line change
@@ -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 PngTransparentColorBehavior
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, let's call it PngTransparentColorMode to keep it in line with othernaming.

{
/// <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,

/// <summary>
/// The transparency will be kept as is.
/// </summary>
Preserve
}
}
52 changes: 52 additions & 0 deletions tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,58 @@ public void Chunk_ComesBeforeIDat(object chunkTypeObj)
}
}

[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)]
Expand Down
8 changes: 3 additions & 5 deletions tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,17 +392,15 @@ public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)

[Theory]
[InlineData(PngColorType.Palette)]
[InlineData(PngColorType.Rgb)]
[InlineData(PngColorType.RgbWithAlpha)]
[InlineData(PngColorType.Grayscale)]
[InlineData(PngColorType.GrayscaleWithAlpha)]
public void Encode_WithMakeTransparentBlackOption_Works(PngColorType colorType)
public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType)
{
// arrange
var image = new Image<Rgba32>(50, 50);
var encoder = new PngEncoder()
{
MakeTransparentBlack = true,
TransparentColorBehavior = PngTransparentColorBehavior.Clear,
ColorType = colorType
};
Rgba32 rgba32 = Color.Blue;
Expand Down Expand Up @@ -442,7 +440,7 @@ public void Encode_WithMakeTransparentBlackOption_Works(PngColorType colorType)

if (y > 25)
{
expectedColor = Color.Black;
expectedColor = Color.Transparent;
}

for (int x = 0; x < actual.Width; x++)
Expand Down