From b8bb3293cb07aa30138a10e506e4e95e1bf73c79 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 87fd2582a..0f416cb7b 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -57,5 +57,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 09575bb28..4442bdb0d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -155,10 +155,15 @@ namespace SixLabors.ImageSharp.Formats.Png 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 dd6c66cb7..5d4be8f14 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,6 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; + this.Optimized = source.Optimized; } /// @@ -78,5 +79,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 9e7fd61ae4ab4ae7562138a55aaba801c71142a9 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 3e46ad29e..adad47f43 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -62,6 +62,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 072dd533ccbbd7abbb5fd4dc94bcb11810afaeec 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 adad47f43..43d7f1ea6 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 2584391bb..05a68a463 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -195,6 +195,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png CompressionLevel = compressionLevel, BitDepth = bitDepth, Quantizer = new WuQuantizer(paletteSize), - InterlaceMethod = interlaceMode + InterlaceMethod = interlaceMode, + Optimized = optimized, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From cff87ea3234f65bcf5f3468021714714d515decb 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 43d7f1ea6..5a79915de 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 4442bdb0d..8833389e7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -153,7 +153,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 45f07ce484b81ed9def9d8a916a966bbbc7758c1 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 0f416cb7b..38c3484c8 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,8 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Png 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 5a79915de..16bd538c3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,9 +63,9 @@ namespace SixLabors.ImageSharp.Formats.Png 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 8833389e7..02bb67c72 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -153,14 +153,26 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 5d4be8f14..89d7b0d5e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; - this.Optimized = source.Optimized; + this.OptimizeMethod = source.OptimizeMethod; } /// @@ -81,8 +81,8 @@ namespace SixLabors.ImageSharp.Formats.Png 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 000000000..7ad664638 --- /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 63fb07c8b29eb4ab81e77118c5aa40ae31350ca4 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 02bb67c72..12a681fea 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -147,13 +147,43 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 c93dd0096493f9f01edec4bff54289a7bc603ff7 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 05a68a463..c72dbe289 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -225,14 +225,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png BitDepth = bitDepth, Quantizer = new WuQuantizer(paletteSize), InterlaceMethod = interlaceMode, - Optimized = optimized, + OptimizeMethod = optimizeMethod, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From 2855d08b797009a234ac697b1a903a264102054b 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 12a681fea..d8be4c80a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -155,19 +155,17 @@ namespace SixLabors.ImageSharp.Formats.Png 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 15a8af7fa0f1c23315dcb3d7313c8fd192e1974f 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 d8be4c80a..a3cc1d018 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -159,12 +159,14 @@ namespace SixLabors.ImageSharp.Formats.Png { 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 6407eef2e5b0de278a427871614ecca48d70e6a6 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 38c3484c8..d8af4c326 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -61,6 +61,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 7ad664638..49af6ce59 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 0f83d3d42..c417ea872 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 47a377e67..89c0b7f65 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 89d7b0d5e..4728b7ca8 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -83,6 +83,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 ce734f6cf..e2051ea27 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -235,7 +235,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png appendPngColorType: true, appendPixelType: true, appendPngBitDepth: true, - optimizeMethod: PngOptimizeMethod.All); + optimizeMethod: PngChunkFilter.ExcludeAll); } } @@ -589,7 +589,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 f7a6cd2ffad70294f8c6b321d2f2ec8c00d786a7 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 d8af4c326..f5113d3d9 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -61,6 +61,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 c417ea872..d2eba47de 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -63,9 +63,9 @@ namespace SixLabors.ImageSharp.Formats.Png 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 89c0b7f65..1af5929fe 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 4728b7ca8..f11a23269 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.Quantizer = source.Quantizer; this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; - this.OptimizeMethod = source.OptimizeMethod; + this.ChunkFilter = source.ChunkFilter; } /// @@ -83,6 +83,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 000000000..9d28fd89b --- /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 e2051ea27..cf5f5c4db 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 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Png { - public class PngEncoderTests + public partial class PngEncoderTests { private static PngEncoder PngEncoder => new PngEncoder(); @@ -221,7 +219,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } - [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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png BitDepth = bitDepth, Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }), InterlaceMethod = interlaceMode, - OptimizeMethod = optimizeMethod, + ChunkFilter = optimizeMethod, }; string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; From 3e0a38e4ffbe813894e749376eae3694777577d4 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 f5113d3d9..be510b156 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -62,5 +62,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 49af6ce59..f859d44da 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -36,11 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 d2eba47de..197506ffd 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -67,6 +67,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 1af5929fe..416c26b0f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,7 +7,7 @@ using System.Buffers.Binary; 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 f11a23269..ba8a897ce 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -30,6 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; + this.MakeTransparentBlack = source.MakeTransparentBlack; } /// @@ -84,5 +85,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets or sets a the optimize method. /// public PngChunkFilter? ChunkFilter { get; set; } + + /// + public bool MakeTransparentBlack { get; set; } } } From 153b7d25ae65abc9ea5b8efa4bfb970441fd5e01 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 416c26b0f..7d00e20e4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -144,6 +144,34 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 7bb3874a493832ab0d2803516232be13ce1801f8 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 cf5f5c4db..dff6cb8f8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -380,6 +380,68 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [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 bc1e8b63534665632d7a4b1f1fd6e9e3785768df 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 7d00e20e4..d8509548a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -144,7 +144,14 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png /// The 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 fe84e40cacdccb583d1d60ed523144b626dbc0e5 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 f859d44da..04f27fb73 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -39,6 +39,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// All possible optimizations. /// - ExcludeAll = ExcludePhysicalChunk | ExcludeGammaChunk | ExcludeExifChunk | ExcludeTextChunks + ExcludeAll = ~None } } From 4c5f8a7e9fce7dba4f23a3b36237df6106a8088d 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 48fd7447d..e5d0b09cf 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 9d28fd89b..fa1544816 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -158,22 +158,41 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png PngChunkType.Header, PngChunkType.Gamma, PngChunkType.Palette, - PngChunkType.Transparency, PngChunkType.InternationalText, PngChunkType.Text, PngChunkType.CompressedText, From ca8b89789ccb938e5108cdac9f8a98a434567345 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 d8509548a..12ffb2cfc 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -227,12 +227,12 @@ namespace SixLabors.ImageSharp.Formats.Png 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 3f490ca6f..3a8e528cc 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -89,11 +89,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// 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 dde7c1eec54d672eae0af1d247e8e4239eb7f9e1 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 e5d0b09cf..770afa3c5 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -59,7 +59,13 @@ namespace SixLabors.ImageSharp.Formats.Png 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 04f27fb73..31d3012a6 100644 --- a/src/ImageSharp/Formats/Png/PngChunkFilter.cs +++ b/src/ImageSharp/Formats/Png/PngChunkFilter.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 53cec6e4e..062e56c1d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -34,22 +34,19 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 2b43edd12..d0eb1a843 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -30,6 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.Threshold = source.Threshold; this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; + this.IgnoreMetadata = source.IgnoreMetadata; this.MakeTransparentBlack = source.MakeTransparentBlack; } @@ -60,11 +61,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 3a8e528cc..52d7fe6d0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -40,6 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Png 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 fa1544816..0418b36c8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -144,6 +144,58 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [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 89a95a07eeb808842bc32084f125b9b8818b202c 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 770afa3c5..ca9d5757f 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -70,8 +70,9 @@ namespace SixLabors.ImageSharp.Formats.Png 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 062e56c1d..d4ca4b7df 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 12ffb2cfc..05c17f376 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,10 +145,11 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - /// 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 @@ namespace SixLabors.ImageSharp.Formats.Png if (rgba32.A == 0) { - span[x].FromRgba32(Color.Black); + span[x].FromRgba32(Color.Transparent); } } } @@ -224,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 d0eb1a843..e46dafada 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; this.IgnoreMetadata = source.IgnoreMetadata; - this.MakeTransparentBlack = source.MakeTransparentBlack; + this.TransparentColorBehavior = source.TransparentColorBehavior; } /// @@ -68,6 +68,6 @@ namespace SixLabors.ImageSharp.Formats.Png 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 000000000..b459751c4 --- /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 857445260..d29279b29 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -392,17 +392,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [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 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png if (y > 25) { - expectedColor = Color.Black; + expectedColor = Color.Transparent; } for (int x = 0; x < actual.Width; x++) From 86cae368f3f9aacb735827809d688f3ec06e6e8d 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 31d3012a6..4e8b5ab96 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 b459751c4..a288ecab5 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 0418b36c8..fa63f73e0 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 25684356a4279f04cb504aca5bc21705e3a59a25 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 409a306dd..66117371e 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -73,6 +73,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// 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 a7f035617..9b1fc80e0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 ba5dd663d..6c30550c2 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Png 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 @@ namespace SixLabors.ImageSharp.Formats.Png 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 916610674..53e6ee30f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.InterlaceMethod = source.InterlaceMethod; this.ChunkFilter = source.ChunkFilter; this.IgnoreMetadata = source.IgnoreMetadata; - this.TransparentColorBehavior = source.TransparentColorBehavior; + this.TransparentColorMode = source.TransparentColorMode; } /// @@ -68,6 +68,6 @@ namespace SixLabors.ImageSharp.Formats.Png 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 a288ecab5..63967c153 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 419bb8c05..4f2490f9a 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -400,7 +400,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png var image = new Image(50, 50); var encoder = new PngEncoder() { - TransparentColorBehavior = PngTransparentColorBehavior.Clear, + TransparentColorMode = PngTransparentColorMode.Clear, ColorType = colorType }; Rgba32 rgba32 = Color.Blue;