Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,22 @@ internal interface IPngEncoderOptions
/// 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 changes: 44 additions & 0 deletions src/ImageSharp/Formats/Png/PngChunkFilter.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 3 deletions src/ImageSharp/Formats/Png/PngEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,21 @@ 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; }

/// <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>
Expand Down
97 changes: 88 additions & 9 deletions src/ImageSharp/Formats/Png/PngEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -141,10 +141,18 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
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);

Expand All @@ -155,11 +163,13 @@ public void Encode<TPixel>(Image<TPixel> 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();
}

/// <inheritdoc />
Expand All @@ -180,6 +190,55 @@ public void Dispose()
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>
Expand Down Expand Up @@ -602,6 +661,11 @@ private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>
/// <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);
Expand All @@ -614,6 +678,11 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
/// <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;
Expand All @@ -631,6 +700,11 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta)
/// <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++)
{
Expand Down Expand Up @@ -723,6 +797,11 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
/// <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.
Expand Down Expand Up @@ -792,7 +871,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
/// <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;
Expand Down Expand Up @@ -890,8 +969,8 @@ private void AllocateExtBuffers()
/// <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;
Expand All @@ -914,7 +993,7 @@ private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<T
/// <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;
Expand Down
12 changes: 12 additions & 0 deletions src/ImageSharp/Formats/Png/PngEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -57,5 +60,14 @@ public PngEncoderOptions(IPngEncoderOptions source)

/// <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 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/PngTransparentColorMode.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 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,
}
}
Loading