Skip to content

Commit 17ec6ee

Browse files
Merge pull request #1012 from equinox2k/PNGOptimisation
Added: ability to skip unneeded chunks for optimization mode
2 parents b720219 + b817615 commit 17ec6ee

File tree

9 files changed

+626
-140
lines changed

9 files changed

+626
-140
lines changed

src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,22 @@ internal interface IPngEncoderOptions
5757
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
5858
/// </summary>
5959
PngInterlaceMode? InterlaceMethod { get; }
60+
61+
/// <summary>
62+
/// Gets a value indicating whether the metadata should be ignored when the image is being encoded.
63+
/// When set to true, all ancillary chunks will be skipped.
64+
/// </summary>
65+
bool IgnoreMetadata { get; }
66+
67+
/// <summary>
68+
/// Gets the chunk filter method. This allows to filter ancillary chunks.
69+
/// </summary>
70+
PngChunkFilter? ChunkFilter { get; }
71+
72+
/// <summary>
73+
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
74+
/// should be converted to transparent black, which can yield in better compression in some cases.
75+
/// </summary>
76+
PngTransparentColorMode TransparentColorMode { get; }
6077
}
6178
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the GNU Affero General Public License, Version 3.
3+
4+
using System;
5+
6+
namespace SixLabors.ImageSharp.Formats.Png
7+
{
8+
/// <summary>
9+
/// Provides enumeration of available PNG optimization methods.
10+
/// </summary>
11+
[Flags]
12+
public enum PngChunkFilter
13+
{
14+
/// <summary>
15+
/// With the None filter, all chunks will be written.
16+
/// </summary>
17+
None = 0,
18+
19+
/// <summary>
20+
/// Excludes the physical dimension information chunk from encoding.
21+
/// </summary>
22+
ExcludePhysicalChunk = 1 << 0,
23+
24+
/// <summary>
25+
/// Excludes the gamma information chunk from encoding.
26+
/// </summary>
27+
ExcludeGammaChunk = 1 << 1,
28+
29+
/// <summary>
30+
/// Excludes the eXIf chunk from encoding.
31+
/// </summary>
32+
ExcludeExifChunk = 1 << 2,
33+
34+
/// <summary>
35+
/// Excludes the tTXt, iTXt or zTXt chunk from encoding.
36+
/// </summary>
37+
ExcludeTextChunks = 1 << 3,
38+
39+
/// <summary>
40+
/// All ancillary chunks will be excluded.
41+
/// </summary>
42+
ExcludeAll = ~None
43+
}
44+
}

src/ImageSharp/Formats/Png/PngEncoder.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,21 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
3434
/// <inheritdoc/>
3535
public IQuantizer Quantizer { get; set; }
3636

37-
/// <summary>
38-
/// Gets or sets the transparency threshold.
39-
/// </summary>
37+
/// <inheritdoc/>
4038
public byte Threshold { get; set; } = byte.MaxValue;
4139

4240
/// <inheritdoc/>
4341
public PngInterlaceMode? InterlaceMethod { get; set; }
4442

43+
/// <inheritdoc/>
44+
public PngChunkFilter? ChunkFilter { get; set; }
45+
46+
/// <inheritdoc/>
47+
public bool IgnoreMetadata { get; set; }
48+
49+
/// <inheritdoc/>
50+
public PngTransparentColorMode TransparentColorMode { get; set; }
51+
4552
/// <summary>
4653
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
4754
/// </summary>

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using System.IO;
88
using System.Runtime.CompilerServices;
99
using System.Runtime.InteropServices;
10-
using SixLabors.ImageSharp.Advanced;
10+
1111
using SixLabors.ImageSharp.Formats.Png.Chunks;
1212
using SixLabors.ImageSharp.Formats.Png.Filters;
1313
using SixLabors.ImageSharp.Formats.Png.Zlib;
@@ -141,10 +141,18 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
141141
this.height = image.Height;
142142

143143
ImageMetadata metadata = image.Metadata;
144-
PngMetadata pngMetadata = metadata.GetPngMetadata();
144+
145+
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
145146
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
146-
IndexedImageFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
147-
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
147+
Image<TPixel> clonedImage = null;
148+
bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear;
149+
if (clearTransparency)
150+
{
151+
clonedImage = image.Clone();
152+
ClearTransparentPixels(clonedImage);
153+
}
154+
155+
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage);
148156

149157
stream.Write(PngConstants.HeaderBytes);
150158

@@ -155,11 +163,13 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
155163
this.WritePhysicalChunk(stream, metadata);
156164
this.WriteExifChunk(stream, metadata);
157165
this.WriteTextChunks(stream, pngMetadata);
158-
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
166+
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
159167
this.WriteEndChunk(stream);
168+
160169
stream.Flush();
161170

162171
quantized?.Dispose();
172+
clonedImage?.Dispose();
163173
}
164174

165175
/// <inheritdoc />
@@ -180,6 +190,55 @@ public void Dispose()
180190
this.filterBuffer = null;
181191
}
182192

193+
/// <summary>
194+
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
195+
/// </summary>
196+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
197+
/// <param name="image">The cloned image where the transparent pixels will be changed.</param>
198+
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
199+
where TPixel : unmanaged, IPixel<TPixel>
200+
{
201+
Rgba32 rgba32 = default;
202+
for (int y = 0; y < image.Height; y++)
203+
{
204+
Span<TPixel> span = image.GetPixelRowSpan(y);
205+
for (int x = 0; x < image.Width; x++)
206+
{
207+
span[x].ToRgba32(ref rgba32);
208+
209+
if (rgba32.A == 0)
210+
{
211+
span[x].FromRgba32(Color.Transparent);
212+
}
213+
}
214+
}
215+
}
216+
217+
/// <summary>
218+
/// Creates the quantized image and sets calculates and sets the bit depth.
219+
/// </summary>
220+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
221+
/// <param name="image">The image to quantize.</param>
222+
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
223+
/// <returns>The quantized image.</returns>
224+
private IndexedImageFrame<TPixel> CreateQuantizedImage<TPixel>(Image<TPixel> image, Image<TPixel> clonedImage)
225+
where TPixel : unmanaged, IPixel<TPixel>
226+
{
227+
IndexedImageFrame<TPixel> quantized;
228+
if (this.options.TransparentColorMode == PngTransparentColorMode.Clear)
229+
{
230+
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage);
231+
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
232+
}
233+
else
234+
{
235+
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
236+
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
237+
}
238+
239+
return quantized;
240+
}
241+
183242
/// <summary>Collects a row of grayscale pixels.</summary>
184243
/// <typeparam name="TPixel">The pixel format.</typeparam>
185244
/// <param name="rowSpan">The image row span.</param>
@@ -602,6 +661,11 @@ private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>
602661
/// <param name="meta">The image metadata.</param>
603662
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
604663
{
664+
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
665+
{
666+
return;
667+
}
668+
605669
PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer);
606670

607671
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size);
@@ -614,6 +678,11 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
614678
/// <param name="meta">The image metadata.</param>
615679
private void WriteExifChunk(Stream stream, ImageMetadata meta)
616680
{
681+
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
682+
{
683+
return;
684+
}
685+
617686
if (meta.ExifProfile is null || meta.ExifProfile.Values.Count == 0)
618687
{
619688
return;
@@ -631,6 +700,11 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta)
631700
/// <param name="meta">The image metadata.</param>
632701
private void WriteTextChunks(Stream stream, PngMetadata meta)
633702
{
703+
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
704+
{
705+
return;
706+
}
707+
634708
const int MaxLatinCode = 255;
635709
for (int i = 0; i < meta.TextData.Count; i++)
636710
{
@@ -723,6 +797,11 @@ private byte[] GetCompressedTextBytes(byte[] textBytes)
723797
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
724798
private void WriteGammaChunk(Stream stream)
725799
{
800+
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
801+
{
802+
return;
803+
}
804+
726805
if (this.options.Gamma > 0)
727806
{
728807
// 4-byte unsigned integer of gamma * 100,000.
@@ -792,7 +871,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
792871
/// <param name="pixels">The image.</param>
793872
/// <param name="quantized">The quantized pixel data. Can be null.</param>
794873
/// <param name="stream">The stream.</param>
795-
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
874+
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
796875
where TPixel : unmanaged, IPixel<TPixel>
797876
{
798877
byte[] buffer;
@@ -890,8 +969,8 @@ private void AllocateExtBuffers()
890969
/// <param name="pixels">The pixels.</param>
891970
/// <param name="quantized">The quantized pixels span.</param>
892971
/// <param name="deflateStream">The deflate stream.</param>
893-
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
894-
where TPixel : unmanaged, IPixel<TPixel>
972+
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
973+
where TPixel : unmanaged, IPixel<TPixel>
895974
{
896975
int bytesPerScanline = this.CalculateScanlineLength(this.width);
897976
int resultLength = bytesPerScanline + 1;
@@ -914,7 +993,7 @@ private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<T
914993
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
915994
/// <param name="pixels">The pixels.</param>
916995
/// <param name="deflateStream">The deflate stream.</param>
917-
private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> pixels, ZlibDeflateStream deflateStream)
996+
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> pixels, ZlibDeflateStream deflateStream)
918997
where TPixel : unmanaged, IPixel<TPixel>
919998
{
920999
int width = pixels.Width;

src/ImageSharp/Formats/Png/PngEncoderOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public PngEncoderOptions(IPngEncoderOptions source)
2929
this.Quantizer = source.Quantizer;
3030
this.Threshold = source.Threshold;
3131
this.InterlaceMethod = source.InterlaceMethod;
32+
this.ChunkFilter = source.ChunkFilter;
33+
this.IgnoreMetadata = source.IgnoreMetadata;
34+
this.TransparentColorMode = source.TransparentColorMode;
3235
}
3336

3437
/// <inheritdoc/>
@@ -57,5 +60,14 @@ public PngEncoderOptions(IPngEncoderOptions source)
5760

5861
/// <inheritdoc/>
5962
public PngInterlaceMode? InterlaceMethod { get; set; }
63+
64+
/// <inheritdoc/>
65+
public PngChunkFilter? ChunkFilter { get; set; }
66+
67+
/// <inheritdoc/>
68+
public bool IgnoreMetadata { get; set; }
69+
70+
/// <inheritdoc/>
71+
public PngTransparentColorMode TransparentColorMode { get; set; }
6072
}
6173
}

src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public static void AdjustOptions<TPixel>(
4040
use16Bit = options.BitDepth == PngBitDepth.Bit16;
4141
bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit);
4242

43+
if (options.IgnoreMetadata)
44+
{
45+
options.ChunkFilter = PngChunkFilter.ExcludeAll;
46+
}
47+
4348
// Ensure we are not allowing impossible combinations.
4449
if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value))
4550
{
@@ -89,11 +94,9 @@ public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
8994
/// </summary>
9095
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
9196
/// <param name="options">The options.</param>
92-
/// <param name="image">The image.</param>
9397
/// <param name="quantizedFrame">The quantized frame.</param>
9498
public static byte CalculateBitDepth<TPixel>(
9599
PngEncoderOptions options,
96-
Image<TPixel> image,
97100
IndexedImageFrame<TPixel> quantizedFrame)
98101
where TPixel : unmanaged, IPixel<TPixel>
99102
{
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the GNU Affero General Public License, Version 3.
3+
4+
namespace SixLabors.ImageSharp.Formats.Png
5+
{
6+
/// <summary>
7+
/// Enum indicating how the transparency should be handled on encoding.
8+
/// </summary>
9+
public enum PngTransparentColorMode
10+
{
11+
/// <summary>
12+
/// The transparency will be kept as is.
13+
/// </summary>
14+
Preserve = 0,
15+
16+
/// <summary>
17+
/// Converts fully transparent pixels that may contain R, G, B values which are not 0,
18+
/// to transparent black, which can yield in better compression in some cases.
19+
/// </summary>
20+
Clear = 1,
21+
}
22+
}

0 commit comments

Comments
 (0)