From e429b812ccf9a755f06bdedb8201304ae34432fc Mon Sep 17 00:00:00 2001 From: Dirk Lemstra Date: Mon, 20 Feb 2017 23:37:08 +0100 Subject: [PATCH] Implemented the IEncoderOptions inside the jpeg encoder. --- .../IJpegEncoderOptions.cs | 29 +++++++ .../ImageExtensions.cs | 11 ++- src/ImageSharp.Formats.Jpeg/JpegEncoder.cs | 75 ++++--------------- .../JpegEncoderCore.cs | 40 +++++++--- .../JpegEncoderOptions.cs | 62 +++++++++++++++ .../Formats/Jpg/JpegDecoderTests.cs | 5 +- .../Formats/Jpg/JpegEncoderTests.cs | 67 +++++++++++++++-- .../Formats/Jpg/JpegProfilingBenchmarks.cs | 5 +- .../TestUtilities/ImagingTestCaseUtility.cs | 5 +- 9 files changed, 209 insertions(+), 90 deletions(-) create mode 100644 src/ImageSharp.Formats.Jpeg/IJpegEncoderOptions.cs create mode 100644 src/ImageSharp.Formats.Jpeg/JpegEncoderOptions.cs diff --git a/src/ImageSharp.Formats.Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp.Formats.Jpeg/IJpegEncoderOptions.cs new file mode 100644 index 000000000..7d62dd4ef --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/IJpegEncoderOptions.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Encapsulates the options for the . + /// + public interface IJpegEncoderOptions : IEncoderOptions + { + /// + /// Gets the quality, that will be used to encode the image. Quality + /// index must be between 0 and 100 (compression from max to min). + /// + /// + /// If the quality is less than or equal to 80, the subsampling ratio will switch to + /// + /// The quality of the jpg image from 0 to 100. + int Quality { get; } + + /// + /// Gets the subsample ration, that will be used to encode the image. + /// + /// The subsample ratio of the jpg image. + JpegSubsample? Subsample { get; } + } +} diff --git a/src/ImageSharp.Formats.Jpeg/ImageExtensions.cs b/src/ImageSharp.Formats.Jpeg/ImageExtensions.cs index 2cbba02a9..311bcc643 100644 --- a/src/ImageSharp.Formats.Jpeg/ImageExtensions.cs +++ b/src/ImageSharp.Formats.Jpeg/ImageExtensions.cs @@ -21,13 +21,18 @@ namespace ImageSharp /// The pixel format. /// The image this method extends. /// The stream to save the image to. - /// The quality to save the image to. Between 1 and 100. + /// The options for the encoder. /// Thrown if the stream is null. /// /// The . /// - public static Image SaveAsJpeg(this Image source, Stream stream, int quality = 75) + public static Image SaveAsJpeg(this Image source, Stream stream, IJpegEncoderOptions options = null) where TColor : struct, IPixel - => source.Save(stream, new JpegEncoder { Quality = quality }); + { + JpegEncoder encoder = new JpegEncoder(); + encoder.Encode(source, stream, options); + + return source; + } } } diff --git a/src/ImageSharp.Formats.Jpeg/JpegEncoder.cs b/src/ImageSharp.Formats.Jpeg/JpegEncoder.cs index 46972cd93..2f2823fa2 100644 --- a/src/ImageSharp.Formats.Jpeg/JpegEncoder.cs +++ b/src/ImageSharp.Formats.Jpeg/JpegEncoder.cs @@ -5,7 +5,6 @@ namespace ImageSharp.Formats { - using System; using System.IO; /// @@ -13,73 +12,27 @@ namespace ImageSharp.Formats /// public class JpegEncoder : IImageEncoder { - /// - /// The quality used to encode the image. - /// - private int quality = 75; - - /// - /// The subsamples scheme used to encode the image. - /// - private JpegSubsample subsample = JpegSubsample.Ratio420; - - /// - /// Whether subsampling has been specifically set. - /// - private bool subsampleSet; - - /// - /// Gets or sets the quality, that will be used to encode the image. Quality - /// index must be between 0 and 100 (compression from max to min). - /// - /// - /// If the quality is less than or equal to 80, the subsampling ratio will switch to - /// - /// The quality of the jpg image from 0 to 100. - public int Quality + /// + public void Encode(Image image, Stream stream, IEncoderOptions options) + where TColor : struct, IPixel { - get { return this.quality; } - set { this.quality = value.Clamp(1, 100); } + IJpegEncoderOptions gifOptions = JpegEncoderOptions.Create(options); + + this.Encode(image, stream, gifOptions); } /// - /// Gets or sets the subsample ration, that will be used to encode the image. + /// Encodes the image to the specified stream from the . /// - /// The subsample ratio of the jpg image. - public JpegSubsample Subsample - { - get - { - return this.subsample; - } - - set - { - this.subsample = value; - this.subsampleSet = true; - } - } - - /// - public void Encode(Image image, Stream stream, IEncoderOptions options) + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The options for the encoder. + public void Encode(Image image, Stream stream, IJpegEncoderOptions options) where TColor : struct, IPixel { - // Ensure that quality can be set but has a fallback. - if (image.MetaData.Quality > 0) - { - this.Quality = image.MetaData.Quality; - } - - JpegEncoderCore encode = new JpegEncoderCore(); - if (this.subsampleSet) - { - encode.Encode(image, stream, this.Quality, this.Subsample); - } - else - { - // Use 4:2:0 Subsampling at quality < 91% for reduced filesize. - encode.Encode(image, stream, this.Quality, this.Quality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); - } + JpegEncoderCore encode = new JpegEncoderCore(options); + encode.Encode(image, stream); } } } diff --git a/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs b/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs index 0309af129..66f400c01 100644 --- a/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs @@ -118,6 +118,11 @@ namespace ImageSharp.Formats /// private readonly byte[] huffmanBuffer = new byte[179]; + /// + /// The options for the encoder. + /// + private readonly IJpegEncoderOptions options; + /// /// The accumulated bits to write to the stream. /// @@ -148,15 +153,22 @@ namespace ImageSharp.Formats /// private JpegSubsample subsample; + /// + /// Initializes a new instance of the class. + /// + /// The options for the encoder. + public JpegEncoderCore(IJpegEncoderOptions options) + { + this.options = options ?? new JpegEncoderOptions(); + } + /// /// Encode writes the image to the jpeg baseline format with the given options. /// /// The pixel format. /// The image to write from. /// The stream to write to. - /// The quality. - /// The subsampling mode. - public void Encode(Image image, Stream stream, int quality, JpegSubsample sample) + public void Encode(Image image, Stream stream) where TColor : struct, IPixel { Guard.NotNull(image, nameof(image)); @@ -168,18 +180,17 @@ namespace ImageSharp.Formats throw new ImageFormatException($"Image is too large to encode at {image.Width}x{image.Height}."); } - this.outputStream = stream; - this.subsample = sample; - - if (quality < 1) + // Ensure that quality can be set but has a fallback. + int quality = this.options.Quality > 0 ? this.options.Quality : image.MetaData.Quality; + if (quality == 0) { - quality = 1; + quality = 75; } - if (quality > 100) - { - quality = 100; - } + quality = quality.Clamp(1, 100); + + this.outputStream = stream; + this.subsample = this.options.Subsample ?? (quality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); // Convert from a quality rating to a scaling factor. int scale; @@ -706,6 +717,11 @@ namespace ImageSharp.Formats private void WriteProfiles(Image image) where TColor : struct, IPixel { + if (this.options.IgnoreMetadata) + { + return; + } + image.MetaData.SyncProfiles(); this.WriteProfile(image.MetaData.ExifProfile); } diff --git a/src/ImageSharp.Formats.Jpeg/JpegEncoderOptions.cs b/src/ImageSharp.Formats.Jpeg/JpegEncoderOptions.cs new file mode 100644 index 000000000..6be36627c --- /dev/null +++ b/src/ImageSharp.Formats.Jpeg/JpegEncoderOptions.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats +{ + /// + /// Encapsulates the options for the . + /// + public sealed class JpegEncoderOptions : EncoderOptions, IJpegEncoderOptions + { + /// + /// Initializes a new instance of the class. + /// + public JpegEncoderOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options for the encoder. + private JpegEncoderOptions(IEncoderOptions options) + : base(options) + { + } + + /// + /// Gets or sets the quality, that will be used to encode the image. Quality + /// index must be between 0 and 100 (compression from max to min). + /// + /// + /// If the quality is less than or equal to 80, the subsampling ratio will switch to + /// + /// The quality of the jpg image from 0 to 100. + public int Quality { get; set; } + + /// + /// Gets or sets the subsample ration, that will be used to encode the image. + /// + /// The subsample ratio of the jpg image. + public JpegSubsample? Subsample { get; set; } + + /// + /// Converts the options to a instance with a + /// cast or by creating a new instance with the specfied options. + /// + /// The options for the encoder. + /// The options for the . + internal static IJpegEncoderOptions Create(IEncoderOptions options) + { + IJpegEncoderOptions jpegOptions = options as IJpegEncoderOptions; + if (jpegOptions != null) + { + return jpegOptions; + } + + return new JpegEncoderOptions(options); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index b8ec9d49e..476de95ff 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -61,12 +61,13 @@ namespace ImageSharp.Tests byte[] data; using (Image image = provider.GetImage()) { - JpegEncoder encoder = new JpegEncoder() { Subsample = subsample, Quality = quality }; + JpegEncoder encoder = new JpegEncoder(); + JpegEncoderOptions options = new JpegEncoderOptions { Subsample = subsample, Quality = quality }; data = new byte[65536]; using (MemoryStream ms = new MemoryStream(data)) { - image.Save(ms, encoder); + image.Save(ms, encoder, options); } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 6518c3e6b..741e785c0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -38,11 +38,12 @@ namespace ImageSharp.Tests { image.MetaData.Quality = quality; image.MetaData.ExifProfile = null; // Reduce the size of the file - JpegEncoder encoder = new JpegEncoder { Subsample = subsample, Quality = quality }; + JpegEncoder encoder = new JpegEncoder(); + JpegEncoderOptions options = new JpegEncoderOptions { Subsample = subsample, Quality = quality }; provider.Utility.TestName += $"{subsample}_Q{quality}"; provider.Utility.SaveTestOutputFile(image, "png"); - provider.Utility.SaveTestOutputFile(image, "jpg", encoder); + provider.Utility.SaveTestOutputFile(image, "jpg", encoder, options); } } @@ -59,13 +60,63 @@ namespace ImageSharp.Tests using (FileStream outputStream = File.OpenWrite(utility.GetTestOutputFileName("jpg"))) { - JpegEncoder encoder = new JpegEncoder() - { - Subsample = subSample, - Quality = quality - }; + JpegEncoder encoder = new JpegEncoder(); - image.Save(outputStream, encoder); + image.Save(outputStream, encoder, new JpegEncoderOptions() + { + Subsample = subSample, + Quality = quality + }); + } + } + } + + [Fact] + public void Encode_IgnoreMetadataIsFalse_ExifProfileIsWritten() + { + var options = new EncoderOptions() + { + IgnoreMetadata = false + }; + + TestFile testFile = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan); + + using (Image input = testFile.CreateImage()) + { + using (MemoryStream memStream = new MemoryStream()) + { + input.Save(memStream, new JpegFormat(), options); + + memStream.Position = 0; + using (Image output = new Image(memStream)) + { + Assert.NotNull(output.MetaData.ExifProfile); + } + } + } + } + + [Fact] + public void Encode_IgnoreMetadataIsTrue_ExifProfileIgnored() + { + var options = new JpegEncoderOptions() + { + IgnoreMetadata = true + }; + + TestFile testFile = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan); + + using (Image input = testFile.CreateImage()) + { + using (MemoryStream memStream = new MemoryStream()) + { + input.SaveAsJpeg(memStream, options); + + memStream.Position = 0; + using (Image output = new Image(memStream)) + { + Assert.Null(output.MetaData.ExifProfile); + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs index bfe1f1e76..50e678bf0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs @@ -81,8 +81,9 @@ namespace ImageSharp.Tests { foreach (Image img in testImages) { - JpegEncoder encoder = new JpegEncoder() { Quality = quality, Subsample = subsample }; - img.Save(ms, encoder); + JpegEncoder encoder = new JpegEncoder(); + JpegEncoderOptions options = new JpegEncoderOptions { Quality = quality, Subsample = subsample }; + img.Save(ms, encoder, options); ms.Seek(0, SeekOrigin.Begin); } }, diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 54be62d37..38429b278 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -75,7 +75,8 @@ namespace ImageSharp.Tests /// The image instance /// The requested extension /// Optional encoder - public void SaveTestOutputFile(Image image, string extension = null, IImageEncoder encoder = null) + /// Optional encoder options + public void SaveTestOutputFile(Image image, string extension = null, IImageEncoder encoder = null, IEncoderOptions options = null) where TColor : struct, IPixel { string path = this.GetTestOutputFileName(extension); @@ -86,7 +87,7 @@ namespace ImageSharp.Tests using (var stream = File.OpenWrite(path)) { - image.Save(stream, encoder); + image.Save(stream, encoder, options); } }