From 9d62a2c989957e8ca67cdd2400f6dcd267db8324 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Thu, 19 Sep 2019 09:30:13 -0700 Subject: [PATCH 01/22] Added: ability to skip unneeded chunks for optimization mode --- src/ImageSharp/Formats/Png/IPngEncoderOptions.cs | 5 +++++ src/ImageSharp/Formats/Png/PngEncoderCore.cs | 13 +++++++++---- src/ImageSharp/Formats/Png/PngEncoderOptions.cs | 6 ++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 87fd2582a5..0f416cb7b0 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -57,5 +57,10 @@ internal interface IPngEncoderOptions /// Gets a value indicating whether this instance should write an Adam7 interlaced image. /// PngInterlaceMode? InterlaceMethod { get; } + + /// + /// Gets a value indicating whether this instance should skip certain chunks to decrease file size + /// + bool Optimized { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 09575bb288..4442bdb0dd 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -155,10 +155,15 @@ public void Encode(Image image, Stream stream) this.WriteHeaderChunk(stream); this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); - this.WritePhysicalChunk(stream, metadata); - this.WriteGammaChunk(stream); - this.WriteExifChunk(stream, metadata); - this.WriteTextChunks(stream, pngMetadata); + + if (!this.options.Optimized) + { + this.WritePhysicalChunk(stream, metadata); + this.WriteGammaChunk(stream); + this.WriteExifChunk(stream, metadata); + this.WriteTextChunks(stream, pngMetadata); + } + this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index dd6c66cb7c..5d4be8f149 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,6 +29,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; + this.Optimized = source.Optimized; } /// @@ -78,5 +79,10 @@ public PngEncoderOptions(IPngEncoderOptions source) /// Gets or sets a value indicating whether this instance should write an Adam7 interlaced image. /// public PngInterlaceMode? InterlaceMethod { get; set; } + + /// + /// Gets or sets a value indicating whether this instance should skip certain chunks to decrease file size + /// + public bool Optimized { get; set; } } } From 69f01c3df5c9316988d20e7788c9ea707e8305f1 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Thu, 19 Sep 2019 10:21:41 -0700 Subject: [PATCH 02/22] Update: forgot to implement Optimized property --- src/ImageSharp/Formats/Png/PngEncoder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 3e46ad29ec..adad47f436 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -62,6 +62,11 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions /// public PngInterlaceMode? InterlaceMethod { get; set; } + /// + /// Gets a value indicating whether this instance should skip certain chunks to decrease file size + /// + public bool Optimized { get; } + /// /// Encodes the image to the specified stream from the . /// From 9ee2066713a99afc36cc0a3bb9dff116ba5fd0f6 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Thu, 19 Sep 2019 11:22:37 -0700 Subject: [PATCH 03/22] Added: test + fixed optimized property --- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- .../Formats/Png/PngEncoderTests.cs | 40 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index adad47f436..43d7f1ea68 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -65,7 +65,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions /// /// Gets a value indicating whether this instance should skip certain chunks to decrease file size /// - public bool Optimized { get; } + public bool Optimized { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 2584391bb7..05a68a463b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -195,6 +195,40 @@ public void WorksWithAllBitDepths(TestImageProvider provider, Pn } } + [Theory] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)] + public void WorksWithAllBitDepthsOptimized(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) + where TPixel : struct, IPixel + { + foreach (PngInterlaceMode interlaceMode in InterlaceMode) + { + TestPngEncoderCore( + provider, + pngColorType, + PngFilterMethod.Adaptive, + pngBitDepth, + interlaceMode, + appendPngColorType: true, + appendPixelType: true, + appendPngBitDepth: true, + optimized: true); + } + } + [Theory] [WithFile(TestImages.Png.Palette8Bpp, nameof(PaletteLargeOnly), PixelTypes.Rgba32)] public void PaletteColorType_WuQuantizer(TestImageProvider provider, int paletteSize) @@ -356,7 +390,8 @@ private static void TestPngEncoderCore( bool appendPixelType = false, bool appendCompressionLevel = false, bool appendPaletteSize = false, - bool appendPngBitDepth = false) + bool appendPngBitDepth = false, + bool optimized = false) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -368,7 +403,8 @@ private static void TestPngEncoderCore( CompressionLevel = compressionLevel, BitDepth = bitDepth, Quantizer = new WuQuantizer(paletteSize), - InterlaceMethod = interlaceMode + InterlaceMethod = interlaceMode, + Optimized = optimized, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From 8d9fbb78697075450a6692c706561057353ba65f Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Thu, 19 Sep 2019 16:52:56 -0700 Subject: [PATCH 04/22] Fixed property documentation + Optimized transparency --- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 43d7f1ea68..5a79915de1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,7 +63,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public PngInterlaceMode? InterlaceMethod { get; set; } /// - /// Gets a value indicating whether this instance should skip certain chunks to decrease file size + /// Gets or sets a value indicating whether this instance should skip certain chunks to decrease file size /// public bool Optimized { get; set; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 4442bdb0dd..8833389e7c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -153,7 +153,7 @@ public void Encode(Image image, Stream stream) stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); this.WriteHeaderChunk(stream); - this.WritePaletteChunk(stream, quantized); + this.WritePaletteChunk(stream, quantized, this.options.Optimized); this.WriteTransparencyChunk(stream, pngMetadata); if (!this.options.Optimized) @@ -552,7 +552,8 @@ private void WriteHeaderChunk(Stream stream) /// The pixel format. /// The containing image data. /// The quantized frame. - private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized) + /// If optimized make fully transparent pixels black. + private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized, bool optimized) where TPixel : struct, IPixel { if (quantized == null) @@ -584,9 +585,9 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu byte alpha = rgba.A; - Unsafe.Add(ref colorTableRef, offset) = rgba.R; - Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; - Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; + Unsafe.Add(ref colorTableRef, offset) = optimized && alpha == 0 ? (byte)0 : rgba.R; + Unsafe.Add(ref colorTableRef, offset + 1) = optimized && alpha == 0 ? (byte)0 : rgba.G; + Unsafe.Add(ref colorTableRef, offset + 2) = optimized && alpha == 0 ? (byte)0 : rgba.B; if (alpha > this.options.Threshold) { From 81bc719c211029cfe2f5b6fe9e0a3ee769cb165c Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Fri, 20 Sep 2019 07:31:55 -0700 Subject: [PATCH 05/22] Update: expanded optimize options --- .../Formats/Png/IPngEncoderOptions.cs | 4 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 4 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 37 +++++++++++--- .../Formats/Png/PngEncoderOptions.cs | 6 +-- .../Formats/Png/PngOptimizeMethod.cs | 49 +++++++++++++++++++ 5 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/PngOptimizeMethod.cs diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 0f416cb7b0..38c3484c81 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,8 +59,8 @@ internal interface IPngEncoderOptions PngInterlaceMode? InterlaceMethod { get; } /// - /// Gets a value indicating whether this instance should skip certain chunks to decrease file size + /// Gets the optimize method. /// - bool Optimized { get; } + PngOptimizeMethod? OptimizeMethod { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 5a79915de1..16bd538c31 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,9 +63,9 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public PngInterlaceMode? InterlaceMethod { get; set; } /// - /// Gets or sets a value indicating whether this instance should skip certain chunks to decrease file size + /// Gets or sets the optimize method. /// - public bool Optimized { get; set; } + public PngOptimizeMethod? OptimizeMethod { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8833389e7c..02bb67c728 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -153,14 +153,26 @@ public void Encode(Image image, Stream stream) stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); this.WriteHeaderChunk(stream); - this.WritePaletteChunk(stream, quantized, this.options.Optimized); + this.WritePaletteChunk(stream, quantized, this.options.OptimizeMethod); this.WriteTransparencyChunk(stream, pngMetadata); - if (!this.options.Optimized) + if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressPhysicalChunk) != PngOptimizeMethod.SuppressPhysicalChunk) { this.WritePhysicalChunk(stream, metadata); + } + + if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressGammaChunk) != PngOptimizeMethod.SuppressGammaChunk) + { this.WriteGammaChunk(stream); + } + + if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressExifChunk) != PngOptimizeMethod.SuppressExifChunk) + { this.WriteExifChunk(stream, metadata); + } + + if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressTextChunks) != PngOptimizeMethod.SuppressTextChunks) + { this.WriteTextChunks(stream, pngMetadata); } @@ -552,8 +564,8 @@ private void WriteHeaderChunk(Stream stream) /// The pixel format. /// The containing image data. /// The quantized frame. - /// If optimized make fully transparent pixels black. - private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized, bool optimized) + /// The optimize method. + private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized, PngOptimizeMethod? optimizeMethod) where TPixel : struct, IPixel { if (quantized == null) @@ -576,6 +588,8 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu Rgba32 rgba = default; + bool makeTransparentBlack = ((optimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.MakeTransparentBlack) == PngOptimizeMethod.MakeTransparentBlack; + for (int i = 0; i < paletteLength; i++) { if (quantizedSpan.IndexOf((byte)i) > -1) @@ -585,9 +599,18 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu byte alpha = rgba.A; - Unsafe.Add(ref colorTableRef, offset) = optimized && alpha == 0 ? (byte)0 : rgba.R; - Unsafe.Add(ref colorTableRef, offset + 1) = optimized && alpha == 0 ? (byte)0 : rgba.G; - Unsafe.Add(ref colorTableRef, offset + 2) = optimized && alpha == 0 ? (byte)0 : rgba.B; + if (makeTransparentBlack && alpha == 0) + { + Unsafe.Add(ref colorTableRef, offset) = 0; + Unsafe.Add(ref colorTableRef, offset + 1) = 0; + Unsafe.Add(ref colorTableRef, offset + 2) = 0; + } + else + { + Unsafe.Add(ref colorTableRef, offset) = rgba.R; + Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; + Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; + } if (alpha > this.options.Threshold) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 5d4be8f149..89d7b0d5ef 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,7 +29,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; - this.Optimized = source.Optimized; + this.OptimizeMethod = source.OptimizeMethod; } /// @@ -81,8 +81,8 @@ public PngEncoderOptions(IPngEncoderOptions source) public PngInterlaceMode? InterlaceMethod { get; set; } /// - /// Gets or sets a value indicating whether this instance should skip certain chunks to decrease file size + /// Gets or sets a the optimize method. /// - public bool Optimized { get; set; } + public PngOptimizeMethod? OptimizeMethod { get; set; } } } diff --git a/src/ImageSharp/Formats/Png/PngOptimizeMethod.cs b/src/ImageSharp/Formats/Png/PngOptimizeMethod.cs new file mode 100644 index 0000000000..7ad6646380 --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngOptimizeMethod.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Provides enumeration of available PNG optimization methods. + /// + [Flags] + public enum PngOptimizeMethod + { + /// + /// With the None filter, the scanline is transmitted unmodified. + /// + None = 0, + + /// + /// Suppress the physical dimension information chunk. + /// + SuppressPhysicalChunk = 1, + + /// + /// Suppress the gamma information chunk. + /// + SuppressGammaChunk = 2, + + /// + /// Suppress the eXIf chunk. + /// + SuppressExifChunk = 4, + + /// + /// Suppress the tTXt, iTXt or zTXt chunk. + /// + SuppressTextChunks = 8, + + /// + /// Make funlly transparent pixels black. + /// + MakeTransparentBlack = 16, + + /// + /// All possible optimizations. + /// + All = 31, + } +} From fc8f5e3eaa0418352a91f1e1ae15d7a84c3b78ba Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Fri, 20 Sep 2019 07:57:53 -0700 Subject: [PATCH 06/22] Update: transparent pixels to black before quantization --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 56 +++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 02bb67c728..12a681feab 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -147,13 +147,43 @@ public void Encode(Image image, Stream stream) ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - IQuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); - this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); + + IQuantizedFrame quantized; + + if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.MakeTransparentBlack) == PngOptimizeMethod.MakeTransparentBlack) + { + using (Image tempImage = image.Clone()) + { + Span span = tempImage.GetPixelSpan(); + foreach (TPixel pixel in span) + { + Rgba32 rgba32 = Rgba32.Transparent; + pixel.ToRgba32(ref rgba32); + + if (rgba32.A == 0) + { + rgba32.R = 0; + rgba32.G = 0; + rgba32.B = 0; + } + + pixel.FromRgba32(rgba32); + } + + quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, tempImage); + this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, tempImage, quantized); + } + } + else + { + quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); + this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); + } stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); this.WriteHeaderChunk(stream); - this.WritePaletteChunk(stream, quantized, this.options.OptimizeMethod); + this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressPhysicalChunk) != PngOptimizeMethod.SuppressPhysicalChunk) @@ -564,8 +594,7 @@ private void WriteHeaderChunk(Stream stream) /// The pixel format. /// The containing image data. /// The quantized frame. - /// The optimize method. - private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized, PngOptimizeMethod? optimizeMethod) + private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized) where TPixel : struct, IPixel { if (quantized == null) @@ -588,8 +617,6 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu Rgba32 rgba = default; - bool makeTransparentBlack = ((optimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.MakeTransparentBlack) == PngOptimizeMethod.MakeTransparentBlack; - for (int i = 0; i < paletteLength; i++) { if (quantizedSpan.IndexOf((byte)i) > -1) @@ -599,18 +626,9 @@ private void WritePaletteChunk(Stream stream, IQuantizedFrame qu byte alpha = rgba.A; - if (makeTransparentBlack && alpha == 0) - { - Unsafe.Add(ref colorTableRef, offset) = 0; - Unsafe.Add(ref colorTableRef, offset + 1) = 0; - Unsafe.Add(ref colorTableRef, offset + 2) = 0; - } - else - { - Unsafe.Add(ref colorTableRef, offset) = rgba.R; - Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; - Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; - } + Unsafe.Add(ref colorTableRef, offset) = rgba.R; + Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; + Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; if (alpha > this.options.Threshold) { From ba08c64755ddea5cdbc02669ba11dac7b5f68e10 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Fri, 20 Sep 2019 08:21:45 -0700 Subject: [PATCH 07/22] Fixed tests --- tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 05a68a463b..c72dbe2891 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -225,14 +225,14 @@ public void WorksWithAllBitDepthsOptimized(TestImageProvider pro appendPngColorType: true, appendPixelType: true, appendPngBitDepth: true, - optimized: true); + optimizeMethod: PngOptimizeMethod.All); } } [Theory] [WithFile(TestImages.Png.Palette8Bpp, nameof(PaletteLargeOnly), PixelTypes.Rgba32)] public void PaletteColorType_WuQuantizer(TestImageProvider provider, int paletteSize) - where TPixel : struct, IPixel + where TPixel : struct, IPixel { foreach (PngInterlaceMode interlaceMode in InterlaceMode) { @@ -391,7 +391,7 @@ private static void TestPngEncoderCore( bool appendCompressionLevel = false, bool appendPaletteSize = false, bool appendPngBitDepth = false, - bool optimized = false) + PngOptimizeMethod optimizeMethod = PngOptimizeMethod.None) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -404,7 +404,7 @@ private static void TestPngEncoderCore( BitDepth = bitDepth, Quantizer = new WuQuantizer(paletteSize), InterlaceMethod = interlaceMode, - Optimized = optimized, + OptimizeMethod = optimizeMethod, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From f8c5277fb4012da6967cc84a0c83c96533cf43d2 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Fri, 20 Sep 2019 09:32:39 -0700 Subject: [PATCH 08/22] Fixed transparency update --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 12a681feab..d8be4c80ac 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -155,19 +155,17 @@ public void Encode(Image image, Stream stream) using (Image tempImage = image.Clone()) { Span span = tempImage.GetPixelSpan(); - foreach (TPixel pixel in span) + for (int i = 0; i < span.Length; i++) { - Rgba32 rgba32 = Rgba32.Transparent; - pixel.ToRgba32(ref rgba32); - + Rgba32 rgba32 = default; + span[i].ToRgba32(ref rgba32); if (rgba32.A == 0) { rgba32.R = 0; rgba32.G = 0; rgba32.B = 0; } - - pixel.FromRgba32(rgba32); + span[i].FromRgba32(rgba32); } quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, tempImage); From 6e3e4ce19fd8573a736bea67cd6b493dff7a85d6 Mon Sep 17 00:00:00 2001 From: Peter Tribe Date: Fri, 20 Sep 2019 10:52:42 -0700 Subject: [PATCH 09/22] Fixed formatting --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index d8be4c80ac..a3cc1d0187 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -159,12 +159,14 @@ public void Encode(Image image, Stream stream) { Rgba32 rgba32 = default; span[i].ToRgba32(ref rgba32); + if (rgba32.A == 0) { rgba32.R = 0; rgba32.G = 0; rgba32.B = 0; } + span[i].FromRgba32(rgba32); } From 6765f961203429d6336099d9639210f744962b7c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 12:35:52 +0200 Subject: [PATCH 10/22] Renamed enum to PngChunkFilter and also renamed enum names --- .../Formats/Png/IPngEncoderOptions.cs | 2 +- ...PngOptimizeMethod.cs => PngChunkFilter.cs} | 24 +++++++++---------- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 10 ++++---- .../Formats/Png/PngEncoderOptions.cs | 2 +- .../Formats/Png/PngEncoderTests.cs | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) rename src/ImageSharp/Formats/Png/{PngOptimizeMethod.cs => PngChunkFilter.cs} (52%) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 38c3484c81..d8af4c3260 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -61,6 +61,6 @@ internal interface IPngEncoderOptions /// /// Gets the optimize method. /// - PngOptimizeMethod? OptimizeMethod { get; } + PngChunkFilter? OptimizeMethod { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngOptimizeMethod.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs similarity index 52% rename from src/ImageSharp/Formats/Png/PngOptimizeMethod.cs rename to src/ImageSharp/Formats/Png/PngChunkFilter.cs index 7ad6646380..49af6ce594 100644 --- a/src/ImageSharp/Formats/Png/PngOptimizeMethod.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -9,41 +9,41 @@ namespace SixLabors.ImageSharp.Formats.Png /// Provides enumeration of available PNG optimization methods. /// [Flags] - public enum PngOptimizeMethod + public enum PngChunkFilter { /// - /// With the None filter, the scanline is transmitted unmodified. + /// With the None filter, all chunks will be written. /// None = 0, /// - /// Suppress the physical dimension information chunk. + /// Excludes the physical dimension information chunk from encoding. /// - SuppressPhysicalChunk = 1, + ExcludePhysicalChunk = 1 << 0, /// - /// Suppress the gamma information chunk. + /// Excludes the gamma information chunk from encoding. /// - SuppressGammaChunk = 2, + ExcludeGammaChunk = 1 << 1, /// - /// Suppress the eXIf chunk. + /// Excludes the eXIf chunk from encoding. /// - SuppressExifChunk = 4, + ExcludeExifChunk = 1 << 2, /// - /// Suppress the tTXt, iTXt or zTXt chunk. + /// Excludes the tTXt, iTXt or zTXt chunk from encoding. /// - SuppressTextChunks = 8, + ExcludeTextChunks = 1 << 3, /// - /// Make funlly transparent pixels black. + /// Make fully transparent pixels black. /// MakeTransparentBlack = 16, /// /// All possible optimizations. /// - All = 31, + ExcludeAll = ExcludePhysicalChunk | ExcludeGammaChunk | ExcludeExifChunk | ExcludeTextChunks } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 0f83d3d42e..c417ea872c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -65,7 +65,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions /// /// Gets or sets the optimize method. /// - public PngOptimizeMethod? OptimizeMethod { get; set; } + public PngChunkFilter? OptimizeMethod { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 47a377e67a..89c0b7f654 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); IndexedImageFrame quantized; - if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.MakeTransparentBlack) == PngOptimizeMethod.MakeTransparentBlack) + if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.MakeTransparentBlack) == PngChunkFilter.MakeTransparentBlack) { using (Image tempImage = image.Clone()) { @@ -182,7 +182,7 @@ public void Encode(Image image, Stream stream) this.WriteHeaderChunk(stream); - if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressGammaChunk) != PngOptimizeMethod.SuppressGammaChunk) + if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) != PngChunkFilter.ExcludeGammaChunk) { this.WriteGammaChunk(stream); } @@ -190,17 +190,17 @@ public void Encode(Image image, Stream stream) this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); - if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressPhysicalChunk) != PngOptimizeMethod.SuppressPhysicalChunk) + if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) != PngChunkFilter.ExcludePhysicalChunk) { this.WritePhysicalChunk(stream, metadata); } - if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressExifChunk) != PngOptimizeMethod.SuppressExifChunk) + if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) != PngChunkFilter.ExcludeExifChunk) { this.WriteExifChunk(stream, metadata); } - if (((this.options.OptimizeMethod ?? PngOptimizeMethod.None) & PngOptimizeMethod.SuppressTextChunks) != PngOptimizeMethod.SuppressTextChunks) + if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) != PngChunkFilter.ExcludeTextChunks) { this.WriteTextChunks(stream, pngMetadata); } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 89d7b0d5ef..4728b7ca83 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -83,6 +83,6 @@ public PngEncoderOptions(IPngEncoderOptions source) /// /// Gets or sets a the optimize method. /// - public PngOptimizeMethod? OptimizeMethod { get; set; } + public PngChunkFilter? OptimizeMethod { get; set; } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index ce734f6cfb..e2051ea277 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -235,7 +235,7 @@ public void WorksWithAllBitDepthsOptimized(TestImageProvider pro appendPngColorType: true, appendPixelType: true, appendPngBitDepth: true, - optimizeMethod: PngOptimizeMethod.All); + optimizeMethod: PngChunkFilter.ExcludeAll); } } @@ -589,7 +589,7 @@ private static void TestPngEncoderCore( bool appendCompressionLevel = false, bool appendPaletteSize = false, bool appendPngBitDepth = false, - PngOptimizeMethod optimizeMethod = PngOptimizeMethod.None) + PngChunkFilter optimizeMethod = PngChunkFilter.None) where TPixel : unmanaged, IPixel { using (Image image = provider.GetImage()) From f257ef2217aa5a0bd8926de98a8191bd41f8bdbe Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 14:05:48 +0200 Subject: [PATCH 11/22] Add tests for exclude filter --- .../Formats/Png/IPngEncoderOptions.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 4 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 10 +- .../Formats/Png/PngEncoderOptions.cs | 4 +- .../Formats/Png/PngEncoderTests.Chunks.cs | 242 ++++++++++++++++++ .../Formats/Png/PngEncoderTests.cs | 128 +-------- 6 files changed, 255 insertions(+), 135 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index d8af4c3260..f5113d3d99 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -61,6 +61,6 @@ internal interface IPngEncoderOptions /// /// Gets the optimize method. /// - PngChunkFilter? OptimizeMethod { get; } + PngChunkFilter? ChunkFilter { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index c417ea872c..d2eba47dea 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,9 +63,9 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public PngInterlaceMode? InterlaceMethod { get; set; } /// - /// Gets or sets the optimize method. + /// Gets or sets the chunk filter. This can be used to exclude some ancillary chunks from being written. /// - public PngChunkFilter? OptimizeMethod { get; set; } + public PngChunkFilter? ChunkFilter { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 89c0b7f654..1af5929fec 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); IndexedImageFrame quantized; - if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.MakeTransparentBlack) == PngChunkFilter.MakeTransparentBlack) + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.MakeTransparentBlack) == PngChunkFilter.MakeTransparentBlack) { using (Image tempImage = image.Clone()) { @@ -182,7 +182,7 @@ public void Encode(Image image, Stream stream) this.WriteHeaderChunk(stream); - if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) != PngChunkFilter.ExcludeGammaChunk) + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) != PngChunkFilter.ExcludeGammaChunk) { this.WriteGammaChunk(stream); } @@ -190,17 +190,17 @@ public void Encode(Image image, Stream stream) this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); - if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) != PngChunkFilter.ExcludePhysicalChunk) + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) != PngChunkFilter.ExcludePhysicalChunk) { this.WritePhysicalChunk(stream, metadata); } - if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) != PngChunkFilter.ExcludeExifChunk) + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) != PngChunkFilter.ExcludeExifChunk) { this.WriteExifChunk(stream, metadata); } - if (((this.options.OptimizeMethod ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) != PngChunkFilter.ExcludeTextChunks) + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) != PngChunkFilter.ExcludeTextChunks) { this.WriteTextChunks(stream, pngMetadata); } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 4728b7ca83..f11a232698 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,7 +29,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; - this.OptimizeMethod = source.OptimizeMethod; + this.ChunkFilter = source.ChunkFilter; } /// @@ -83,6 +83,6 @@ public PngEncoderOptions(IPngEncoderOptions source) /// /// Gets or sets a the optimize method. /// - public PngChunkFilter? OptimizeMethod { get; set; } + public PngChunkFilter? ChunkFilter { get; set; } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs new file mode 100644 index 0000000000..9d28fd89b0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, PngEncoder); + + // assert + memStream.Position = 0; + Span 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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, PngEncoder); + + // assert + memStream.Position = 0; + Span 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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, PngEncoder); + + // assert + memStream.Position = 0; + Span 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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, PngEncoder); + + // assert + memStream.Position = 0; + Span 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); + } + } + + [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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 }; + var excludedChunkTypes = new List(); + switch (chunkFilter) + { + case PngChunkFilter.ExcludeGammaChunk: + excludedChunkTypes.Add(PngChunkType.Gamma); + break; + case PngChunkFilter.ExcludeExifChunk: + excludedChunkTypes.Add(PngChunkType.Exif); + break; + case PngChunkFilter.ExcludePhysicalChunk: + excludedChunkTypes.Add(PngChunkType.Physical); + break; + case PngChunkFilter.ExcludeTextChunks: + excludedChunkTypes.Add(PngChunkType.Text); + excludedChunkTypes.Add(PngChunkType.InternationalText); + excludedChunkTypes.Add(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); + break; + } + + // act + input.Save(memStream, encoder); + + // assert + Assert.True(excludedChunkTypes.Count > 0); + memStream.Position = 0; + Span 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"); + + bytesSpan = bytesSpan.Slice(4 + 4 + length + 4); + } + } + + [Fact] + public void ExcludeFilter_WithNone_DoesNotExcludeChunks() + { + // arrange + var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); + using Image input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 }; + var expectedChunkTypes = new List() + { + PngChunkType.Header, + PngChunkType.Gamma, + PngChunkType.Palette, + PngChunkType.Transparency, + PngChunkType.InternationalText, + PngChunkType.Text, + PngChunkType.CompressedText, + PngChunkType.Exif, + PngChunkType.Physical, + PngChunkType.Data, + PngChunkType.End, + }; + + // act + input.Save(memStream, encoder); + memStream.Position = 0; + Span 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); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index e2051ea277..cf5f5c4dba 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. // ReSharper disable InconsistentNaming -using System; -using System.Buffers.Binary; using System.IO; using System.Linq; @@ -18,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { - public class PngEncoderTests + public partial class PngEncoderTests { private static PngEncoder PngEncoder => new PngEncoder(); @@ -221,7 +219,7 @@ public void WorksWithAllBitDepths(TestImageProvider provider, Pn [WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)] [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)] [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)] - public void WorksWithAllBitDepthsOptimized(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) + public void WorksWithAllBitDepthsAndExcludeAllFilter(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) where TPixel : unmanaged, IPixel { foreach (PngInterlaceMode interlaceMode in InterlaceMode) @@ -435,126 +433,6 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo } } - [Fact] - public void HeaderChunk_ComesFirst() - { - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); - using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - input.Save(memStream, PngEncoder); - memStream.Position = 0; - - // Skip header. - Span bytesSpan = memStream.ToArray().AsSpan(8); - 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() - { - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); - using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - input.Save(memStream, PngEncoder); - memStream.Position = 0; - - // Skip header. - Span bytesSpan = memStream.ToArray().AsSpan(8); - - 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) - { - var chunkType = (PngChunkType)chunkTypeObj; - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); - using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - input.Save(memStream, PngEncoder); - memStream.Position = 0; - - // Skip header. - Span bytesSpan = memStream.ToArray().AsSpan(8); - - 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) - { - var chunkType = (PngChunkType)chunkTypeObj; - var testFile = TestFile.Create(TestImages.Png.PngWithMetadata); - using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); - input.Save(memStream, PngEncoder); - memStream.Position = 0; - - // Skip header. - Span bytesSpan = memStream.ToArray().AsSpan(8); - - 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); - } - } - [Theory] [WithTestPatternImages(587, 821, PixelTypes.Rgba32)] [WithTestPatternImages(677, 683, PixelTypes.Rgba32)] @@ -602,7 +480,7 @@ private static void TestPngEncoderCore( BitDepth = bitDepth, Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }), InterlaceMethod = interlaceMode, - OptimizeMethod = optimizeMethod, + ChunkFilter = optimizeMethod, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From f04e836587a87524d4cfe0ff19f54b50459c9c79 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 14:29:57 +0200 Subject: [PATCH 12/22] MakeTransparentBlack is now a png encoder option --- src/ImageSharp/Formats/Png/IPngEncoderOptions.cs | 5 +++++ src/ImageSharp/Formats/Png/PngChunkFilter.cs | 5 ----- src/ImageSharp/Formats/Png/PngEncoder.cs | 5 +++++ src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 ++-- src/ImageSharp/Formats/Png/PngEncoderOptions.cs | 4 ++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index f5113d3d99..be510b1562 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -62,5 +62,10 @@ internal interface IPngEncoderOptions /// Gets the optimize method. /// PngChunkFilter? ChunkFilter { get; } + + /// + /// Gets a value indicating whether fully transparent pixels should be converted to black pixels. + /// + bool MakeTransparentBlack { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngChunkFilter.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs index 49af6ce594..f859d44dac 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -36,11 +36,6 @@ public enum PngChunkFilter /// ExcludeTextChunks = 1 << 3, - /// - /// Make fully transparent pixels black. - /// - MakeTransparentBlack = 16, - /// /// All possible optimizations. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index d2eba47dea..197506ffd5 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -67,6 +67,11 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions /// public PngChunkFilter? ChunkFilter { get; set; } + /// + /// Gets or sets a value indicating whether fully transparent pixels should be converted to black pixels. + /// + public bool MakeTransparentBlack { get; set; } + /// /// Encodes the image to the specified stream from the . /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1af5929fec..416c26b0f0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -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; @@ -145,7 +145,7 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); IndexedImageFrame quantized; - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.MakeTransparentBlack) == PngChunkFilter.MakeTransparentBlack) + if (this.options.MakeTransparentBlack) { using (Image tempImage = image.Clone()) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index f11a232698..ba8a897cef 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -30,6 +30,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; + this.MakeTransparentBlack = source.MakeTransparentBlack; } /// @@ -84,5 +85,8 @@ public PngEncoderOptions(IPngEncoderOptions source) /// Gets or sets a the optimize method. /// public PngChunkFilter? ChunkFilter { get; set; } + + /// + public bool MakeTransparentBlack { get; set; } } } From 624591c421069484f6b1a9b5f61cf6b504cb6db6 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 14:44:39 +0200 Subject: [PATCH 13/22] Refactor --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 81 ++++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 416c26b0f0..7d00e20e4b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -144,6 +144,34 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); + IndexedImageFrame quantized = this.CreateQuantizedImage(image); + + stream.Write(PngConstants.HeaderBytes); + + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WritePaletteChunk(stream, quantized); + this.WriteTransparencyChunk(stream, pngMetadata); + this.WritePhysicalChunk(stream, metadata); + this.WriteExifChunk(stream, metadata); + this.WriteTextChunks(stream, pngMetadata); + this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); + this.WriteEndChunk(stream); + + stream.Flush(); + + quantized?.Dispose(); + } + + /// + /// Creates the quantized image and sets calculates and sets the bit depth. + /// + /// The type of the pixel. + /// The image to quantize. + /// The quantized image. + private IndexedImageFrame CreateQuantizedImage(Image image) + where TPixel : unmanaged, IPixel + { IndexedImageFrame quantized; if (this.options.MakeTransparentBlack) { @@ -178,38 +206,7 @@ public void Encode(Image image, Stream stream) this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); } - stream.Write(PngConstants.HeaderBytes); - - this.WriteHeaderChunk(stream); - - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) != PngChunkFilter.ExcludeGammaChunk) - { - this.WriteGammaChunk(stream); - } - - this.WritePaletteChunk(stream, quantized); - this.WriteTransparencyChunk(stream, pngMetadata); - - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) != PngChunkFilter.ExcludePhysicalChunk) - { - this.WritePhysicalChunk(stream, metadata); - } - - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) != PngChunkFilter.ExcludeExifChunk) - { - this.WriteExifChunk(stream, metadata); - } - - if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) != PngChunkFilter.ExcludeTextChunks) - { - this.WriteTextChunks(stream, pngMetadata); - } - - this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); - this.WriteEndChunk(stream); - stream.Flush(); - - quantized?.Dispose(); + return quantized; } /// @@ -652,6 +649,11 @@ private void WritePaletteChunk(Stream stream, IndexedImageFrame /// The image metadata. 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); @@ -664,6 +666,11 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta) /// The image metadata. 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; @@ -681,6 +688,11 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) /// The image metadata. 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++) { @@ -773,6 +785,11 @@ private byte[] GetCompressedTextBytes(byte[] textBytes) /// The containing image data. 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. From a86713f25ebe5ec3dd4036f744aa72fd3d765a7f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 18:48:07 +0200 Subject: [PATCH 14/22] Add tests for make transparent black option --- .../Formats/Png/PngEncoderTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index cf5f5c4dba..dff6cb8f8c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -380,6 +380,68 @@ 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) + { + // arrange + var image = new Image(50, 50); + var encoder = new PngEncoder() + { + MakeTransparentBlack = true, + ColorType = colorType + }; + Rgba32 rgba32 = Color.Blue; + for (int y = 0; y < image.Height; y++) + { + System.Span rowSpan = image.GetPixelRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x].FromRgba32(rgba32); + } + } + + // act + using var memStream = new MemoryStream(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using var actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue; + if (colorType == PngColorType.Grayscale || colorType == PngColorType.GrayscaleWithAlpha) + { + var luminance = ImageMaths.Get8BitBT709Luminance(expectedColor.R, expectedColor.G, expectedColor.B); + expectedColor = new Rgba32(luminance, luminance, luminance); + } + + for (int y = 0; y < actual.Height; y++) + { + System.Span rowSpan = actual.GetPixelRowSpan(y); + + if (y > 25) + { + expectedColor = Color.Black; + } + + for (int x = 0; x < actual.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + } + [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) From 8ca9b97c9e182b7792a25d1d1b3a70b18b35417b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Apr 2020 18:58:43 +0200 Subject: [PATCH 15/22] MakeTransparentBlack option now work with all png color type --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 109 ++++++++++--------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 7d00e20e4b..d8509548ad 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -144,7 +144,14 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - IndexedImageFrame quantized = this.CreateQuantizedImage(image); + Image clonedImage = null; + if (this.options.MakeTransparentBlack) + { + clonedImage = image.Clone(); + MakeTransparentPixelsBlack(clonedImage); + } + + IndexedImageFrame quantized = this.CreateQuantizedImage(image, clonedImage); stream.Write(PngConstants.HeaderBytes); @@ -155,12 +162,55 @@ public void Encode(Image image, Stream stream) this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); + this.WriteDataChunks(this.options.MakeTransparentBlack ? clonedImage : image, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); quantized?.Dispose(); + clonedImage?.Dispose(); + } + + /// + public void Dispose() + { + this.previousScanline?.Dispose(); + this.currentScanline?.Dispose(); + this.subFilter?.Dispose(); + this.averageFilter?.Dispose(); + this.paethFilter?.Dispose(); + this.filterBuffer?.Dispose(); + + this.previousScanline = null; + this.currentScanline = null; + this.subFilter = null; + this.averageFilter = null; + this.paethFilter = null; + this.filterBuffer = null; + } + + /// + /// Makes transparent pixels black. + /// + /// The type of the pixel. + /// The cloned image where the transparent pixels will be changed. + private static void MakeTransparentPixelsBlack(Image image) + where TPixel : unmanaged, IPixel + { + Rgba32 rgba32 = default; + for (int y = 0; y < image.Height; y++) + { + Span 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.Black); + } + } + } } /// @@ -168,37 +218,16 @@ public void Encode(Image image, Stream stream) /// /// The type of the pixel. /// The image to quantize. + /// Cloned image with transparent pixels are changed to black. /// The quantized image. - private IndexedImageFrame CreateQuantizedImage(Image image) + private IndexedImageFrame CreateQuantizedImage(Image image, Image clonedImage) where TPixel : unmanaged, IPixel { IndexedImageFrame quantized; if (this.options.MakeTransparentBlack) { - using (Image tempImage = image.Clone()) - { - for (int y = 0; y < image.Height; y++) - { - Span span = tempImage.GetPixelRowSpan(y); - for (int x = 0; x < image.Width; x++) - { - Rgba32 rgba32 = default; - span[x].ToRgba32(ref rgba32); - - if (rgba32.A == 0) - { - rgba32.R = 0; - rgba32.G = 0; - rgba32.B = 0; - } - - span[x].FromRgba32(rgba32); - } - } - - quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, tempImage); - this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, tempImage, quantized); - } + quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage); + this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, clonedImage, quantized); } else { @@ -209,24 +238,6 @@ private IndexedImageFrame CreateQuantizedImage(Image ima return quantized; } - /// - public void Dispose() - { - this.previousScanline?.Dispose(); - this.currentScanline?.Dispose(); - this.subFilter?.Dispose(); - this.averageFilter?.Dispose(); - this.paethFilter?.Dispose(); - this.filterBuffer?.Dispose(); - - this.previousScanline = null; - this.currentScanline = null; - this.subFilter = null; - this.averageFilter = null; - this.paethFilter = null; - this.filterBuffer = null; - } - /// Collects a row of grayscale pixels. /// The pixel format. /// The image row span. @@ -859,7 +870,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) /// The image. /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, IndexedImageFrame quantized, Stream stream) + private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -957,8 +968,8 @@ private void AllocateExtBuffers() /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(ImageFrame pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) - where TPixel : unmanaged, IPixel + private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + where TPixel : unmanaged, IPixel { int bytesPerScanline = this.CalculateScanlineLength(this.width); int resultLength = bytesPerScanline + 1; @@ -981,7 +992,7 @@ private void EncodePixels(ImageFrame pixels, IndexedImageFrameThe type of the pixel. /// The pixels. /// The deflate stream. - private void EncodeAdam7Pixels(ImageFrame pixels, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(Image pixels, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int width = pixels.Width; From 200ef9ffbeb44d7ad088834a62c6c092fc6e71a8 Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Thu, 30 Apr 2020 11:22:47 +0200 Subject: [PATCH 16/22] ExcludeAll = ~None Co-Authored-By: James Jackson-South --- src/ImageSharp/Formats/Png/PngChunkFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkFilter.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs index f859d44dac..04f27fb73b 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -39,6 +39,6 @@ public enum PngChunkFilter /// /// All possible optimizations. /// - ExcludeAll = ExcludePhysicalChunk | ExcludeGammaChunk | ExcludeExifChunk | ExcludeTextChunks + ExcludeAll = ~None } } From f9c2f6ade67a4f6f2a9e9eeb92d3205ec832730e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 30 Apr 2020 11:20:29 +0200 Subject: [PATCH 17/22] Improve exclude filter test to also check presence of expected chunks --- .../Formats/Png/IPngEncoderOptions.cs | 2 +- .../Formats/Png/PngEncoderTests.Chunks.cs | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 48fd7447dd..e5d0b09cf7 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,7 +59,7 @@ internal interface IPngEncoderOptions PngInterlaceMode? InterlaceMethod { get; } /// - /// Gets the optimize method. + /// Gets chunk filter method. /// PngChunkFilter? ChunkFilter { get; } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 9d28fd89b0..fa15448164 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -158,22 +158,41 @@ public void ExcludeFilter_Works(object filterObj) using Image input = testFile.CreateRgba32Image(); using var memStream = new MemoryStream(); var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 }; + var expectedChunkTypes = new Dictionary() + { + { 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(); 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); @@ -182,6 +201,12 @@ public void ExcludeFilter_Works(object filterObj) 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; } @@ -197,9 +222,19 @@ public void ExcludeFilter_Works(object filterObj) 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] @@ -215,7 +250,6 @@ public void ExcludeFilter_WithNone_DoesNotExcludeChunks() PngChunkType.Header, PngChunkType.Gamma, PngChunkType.Palette, - PngChunkType.Transparency, PngChunkType.InternationalText, PngChunkType.Text, PngChunkType.CompressedText, From 0f581ab5e6c763edc61603b27fdf5c4a5066a4c7 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 May 2020 09:19:23 +0200 Subject: [PATCH 18/22] Remove not needed image parameter from CalculateBitDepth --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 ++-- src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index d8509548ad..12ffb2cfcf 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -227,12 +227,12 @@ private IndexedImageFrame CreateQuantizedImage(Image ima if (this.options.MakeTransparentBlack) { 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; diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 3f490ca6f8..3a8e528cc0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -89,11 +89,9 @@ public static IndexedImageFrame CreateQuantizedFrame( /// /// The type of the pixel. /// The options. - /// The image. /// The quantized frame. public static byte CalculateBitDepth( PngEncoderOptions options, - Image image, IndexedImageFrame quantizedFrame) where TPixel : unmanaged, IPixel { From bc7eb2791d36d5c20ee0be6f38ba3001b7545067 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 May 2020 10:16:02 +0200 Subject: [PATCH 19/22] Add IgnoreMetadata to the png encoder options --- .../Formats/Png/IPngEncoderOptions.cs | 8 ++- src/ImageSharp/Formats/Png/PngChunkFilter.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 15 +++--- .../Formats/Png/PngEncoderOptions.cs | 8 +-- .../Formats/Png/PngEncoderOptionsHelpers.cs | 5 ++ .../Formats/Png/PngEncoderTests.Chunks.cs | 52 +++++++++++++++++++ 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index e5d0b09cf7..770afa3c57 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,7 +59,13 @@ internal interface IPngEncoderOptions PngInterlaceMode? InterlaceMethod { get; } /// - /// 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. + /// + bool IgnoreMetadata { get; } + + /// + /// Gets the chunk filter method. This allows to filter ancillary chunks. /// PngChunkFilter? ChunkFilter { get; } diff --git a/src/ImageSharp/Formats/Png/PngChunkFilter.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs index 04f27fb73b..31d3012a6d 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -37,7 +37,7 @@ public enum PngChunkFilter ExcludeTextChunks = 1 << 3, /// - /// All possible optimizations. + /// All ancillary chunks will be excluded. /// ExcludeAll = ~None } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 53cec6e4ee..062e56c1d3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -34,22 +34,19 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions /// public IQuantizer Quantizer { get; set; } - /// - /// Gets or sets the transparency threshold. - /// + /// public byte Threshold { get; set; } = byte.MaxValue; /// public PngInterlaceMode? InterlaceMethod { get; set; } - /// - /// Gets or sets the chunk filter. This can be used to exclude some ancillary chunks from being written. - /// + /// public PngChunkFilter? ChunkFilter { get; set; } - /// - /// Gets or sets a value indicating whether fully transparent pixels should be converted to black pixels. - /// + /// + public bool IgnoreMetadata { get; set; } + + /// public bool MakeTransparentBlack { get; set; } /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 2b43edd128..d0eb1a843d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -30,6 +30,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; + this.IgnoreMetadata = source.IgnoreMetadata; this.MakeTransparentBlack = source.MakeTransparentBlack; } @@ -60,11 +61,12 @@ public PngEncoderOptions(IPngEncoderOptions source) /// public PngInterlaceMode? InterlaceMethod { get; set; } - /// - /// Gets or sets a the optimize method. - /// + /// public PngChunkFilter? ChunkFilter { get; set; } + /// + public bool IgnoreMetadata { get; set; } + /// public bool MakeTransparentBlack { get; set; } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 3a8e528cc0..52d7fe6d0a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -40,6 +40,11 @@ public static void AdjustOptions( 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)) { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index fa15448164..0418b36c87 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -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 input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 }; + var expectedChunkTypes = new Dictionary() + { + { PngChunkType.Header, false }, + { PngChunkType.Palette, false }, + { PngChunkType.Data, false }, + { PngChunkType.End, false } + }; + var excludedChunkTypes = new List() + { + 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 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)] From ec3656ff7a189acc6adf3d2f7df04aef22d5f4ef Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 May 2020 10:38:47 +0200 Subject: [PATCH 20/22] Add PngTransparentColorBehavior enum --- .../Formats/Png/IPngEncoderOptions.cs | 5 +++-- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 15 +++++++------ .../Formats/Png/PngEncoderOptions.cs | 4 ++-- .../Png/PngTransparentColorBehavior.cs | 22 +++++++++++++++++++ .../Formats/Png/PngEncoderTests.cs | 8 +++---- 6 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 770afa3c57..ca9d5757fe 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -70,8 +70,9 @@ internal interface IPngEncoderOptions PngChunkFilter? ChunkFilter { get; } /// - /// 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. /// - bool MakeTransparentBlack { get; } + PngTransparentColorBehavior TransparentColorBehavior { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 062e56c1d3..d4ca4b7dfe 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -47,7 +47,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public bool IgnoreMetadata { get; set; } /// - public bool MakeTransparentBlack { get; set; } + public PngTransparentColorBehavior TransparentColorBehavior { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 12ffb2cfcf..05c17f376a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,10 +145,11 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); Image clonedImage = null; - if (this.options.MakeTransparentBlack) + bool clearTransparency = this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear; + if (clearTransparency) { clonedImage = image.Clone(); - MakeTransparentPixelsBlack(clonedImage); + ClearTransparentPixels(clonedImage); } IndexedImageFrame quantized = this.CreateQuantizedImage(image, clonedImage); @@ -162,7 +163,7 @@ public void Encode(Image 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(); @@ -190,11 +191,11 @@ public void Dispose() } /// - /// Makes transparent pixels black. + /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// /// The type of the pixel. /// The cloned image where the transparent pixels will be changed. - private static void MakeTransparentPixelsBlack(Image image) + private static void ClearTransparentPixels(Image image) where TPixel : unmanaged, IPixel { Rgba32 rgba32 = default; @@ -207,7 +208,7 @@ private static void MakeTransparentPixelsBlack(Image image) if (rgba32.A == 0) { - span[x].FromRgba32(Color.Black); + span[x].FromRgba32(Color.Transparent); } } } @@ -224,7 +225,7 @@ private IndexedImageFrame CreateQuantizedImage(Image ima where TPixel : unmanaged, IPixel { IndexedImageFrame quantized; - if (this.options.MakeTransparentBlack) + if (this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear) { quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized); diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index d0eb1a843d..e46dafada5 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -31,7 +31,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; this.IgnoreMetadata = source.IgnoreMetadata; - this.MakeTransparentBlack = source.MakeTransparentBlack; + this.TransparentColorBehavior = source.TransparentColorBehavior; } /// @@ -68,6 +68,6 @@ public PngEncoderOptions(IPngEncoderOptions source) public bool IgnoreMetadata { get; set; } /// - public bool MakeTransparentBlack { get; set; } + public PngTransparentColorBehavior TransparentColorBehavior { get; set; } } } diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs b/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs new file mode 100644 index 0000000000..b459751c4b --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs @@ -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 +{ + /// + /// Enum indicating how the transparency should be handled on encoding. + /// + public enum PngTransparentColorBehavior + { + /// + /// 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. + /// + Clear, + + /// + /// The transparency will be kept as is. + /// + Preserve + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 857445260d..d29279b299 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -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(50, 50); var encoder = new PngEncoder() { - MakeTransparentBlack = true, + TransparentColorBehavior = PngTransparentColorBehavior.Clear, ColorType = colorType }; Rgba32 rgba32 = Color.Blue; @@ -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++) From 129b6392d5161c8686aea1cda13349fa78ee231d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 May 2020 10:50:17 +0200 Subject: [PATCH 21/22] Switched Preserve to be 0 for PngTransparentColorBehavior enum --- src/ImageSharp/Formats/Png/PngChunkFilter.cs | 2 +- .../Formats/Png/PngTransparentColorBehavior.cs | 10 +++++----- .../Formats/Png/PngEncoderTests.Chunks.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkFilter.cs b/src/ImageSharp/Formats/Png/PngChunkFilter.cs index 31d3012a6d..4e8b5ab96f 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the GNU Affero General Public License, Version 3. using System; diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs b/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs index b459751c4b..a288ecab5c 100644 --- a/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs +++ b/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs @@ -9,14 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Png public enum PngTransparentColorBehavior { /// - /// 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. + /// The transparency will be kept as is. /// - Clear, + Preserve = 0, /// - /// The transparency will be kept as is. + /// 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. /// - Preserve + Clear = 1, } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 0418b36c87..fa63f73e06 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the GNU Affero General Public License, Version 3. using System; using System.Buffers.Binary; From b69d772659bcac29bdc4b51cb31c29415c97df63 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 1 May 2020 12:12:04 +0200 Subject: [PATCH 22/22] Rename PngTransparentColorBehavior to PngTransparentColorMode --- src/ImageSharp/Formats/Png/IPngEncoderOptions.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 ++-- src/ImageSharp/Formats/Png/PngEncoderOptions.cs | 4 ++-- ...TransparentColorBehavior.cs => PngTransparentColorMode.cs} | 2 +- tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename src/ImageSharp/Formats/Png/{PngTransparentColorBehavior.cs => PngTransparentColorMode.cs} (93%) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 409a306dd3..66117371ed 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -73,6 +73,6 @@ internal interface IPngEncoderOptions /// 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. /// - PngTransparentColorBehavior TransparentColorBehavior { get; } + PngTransparentColorMode TransparentColorMode { get; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index a7f0356178..9b1fc80e07 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -47,7 +47,7 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public bool IgnoreMetadata { get; set; } /// - public PngTransparentColorBehavior TransparentColorBehavior { get; set; } + public PngTransparentColorMode TransparentColorMode { get; set; } /// /// Encodes the image to the specified stream from the . diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ba5dd663df..6c30550c2a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ public void Encode(Image image, Stream stream) PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); Image clonedImage = null; - bool clearTransparency = this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear; + bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear; if (clearTransparency) { clonedImage = image.Clone(); @@ -225,7 +225,7 @@ private IndexedImageFrame CreateQuantizedImage(Image ima where TPixel : unmanaged, IPixel { IndexedImageFrame quantized; - if (this.options.TransparentColorBehavior == PngTransparentColorBehavior.Clear) + if (this.options.TransparentColorMode == PngTransparentColorMode.Clear) { quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized); diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 9166106747..53e6ee30f8 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -31,7 +31,7 @@ public PngEncoderOptions(IPngEncoderOptions source) this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; this.IgnoreMetadata = source.IgnoreMetadata; - this.TransparentColorBehavior = source.TransparentColorBehavior; + this.TransparentColorMode = source.TransparentColorMode; } /// @@ -68,6 +68,6 @@ public PngEncoderOptions(IPngEncoderOptions source) public bool IgnoreMetadata { get; set; } /// - public PngTransparentColorBehavior TransparentColorBehavior { get; set; } + public PngTransparentColorMode TransparentColorMode { get; set; } } } diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs similarity index 93% rename from src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs rename to src/ImageSharp/Formats/Png/PngTransparentColorMode.cs index a288ecab5c..63967c153f 100644 --- a/src/ImageSharp/Formats/Png/PngTransparentColorBehavior.cs +++ b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Enum indicating how the transparency should be handled on encoding. /// - public enum PngTransparentColorBehavior + public enum PngTransparentColorMode { /// /// The transparency will be kept as is. diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 419bb8c056..4f2490f9a6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -400,7 +400,7 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color var image = new Image(50, 50); var encoder = new PngEncoder() { - TransparentColorBehavior = PngTransparentColorBehavior.Clear, + TransparentColorMode = PngTransparentColorMode.Clear, ColorType = colorType }; Rgba32 rgba32 = Color.Blue;