Skip to content

Commit

Permalink
Merge pull request #1012 from equinox2k/PNGOptimisation
Browse files Browse the repository at this point in the history
Added: ability to skip unneeded chunks for optimization mode
  • Loading branch information
JimBobSquarePants authored May 17, 2020
2 parents b720219 + b817615 commit 17ec6ee
Show file tree
Hide file tree
Showing 9 changed files with 626 additions and 140 deletions.
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

0 comments on commit 17ec6ee

Please sign in to comment.