diff --git a/samples/ImageSharp.Web.Sample/Startup.cs b/samples/ImageSharp.Web.Sample/Startup.cs index a820a75c..3a919b54 100644 --- a/samples/ImageSharp.Web.Sample/Startup.cs +++ b/samples/ImageSharp.Web.Sample/Startup.cs @@ -58,7 +58,8 @@ public void ConfigureServices(IServiceCollection services) .AddProvider() .AddProcessor() .AddProcessor() - .AddProcessor(); + .AddProcessor() + .AddProcessor(); // Add the default service and options. // @@ -134,7 +135,8 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services .ClearProcessors() .AddProcessor() .AddProcessor() - .AddProcessor(); + .AddProcessor() + .AddProcessor(); } /// diff --git a/samples/ImageSharp.Web.Sample/wwwroot/index.html b/samples/ImageSharp.Web.Sample/wwwroot/index.html index 8ecb8aa8..e9c922d0 100644 --- a/samples/ImageSharp.Web.Sample/wwwroot/index.html +++ b/samples/ImageSharp.Web.Sample/wwwroot/index.html @@ -128,6 +128,34 @@

Format


+
+

Jpeg Quality

+
+

+ imagesharp-logo.png?width=300&format=jpg&quality=100 +

+

+ +

+
+
+

+ imagesharp-logo.png?width=300&format=jpg&quality=50 +

+

+ +

+
+
+

+ imagesharp-logo.png?width=300&format=jpg&quality=1 +

+

+ +

+
+
+

Background Color

diff --git a/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs b/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs index 2cef0e80..9f3447ff 100644 --- a/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs @@ -67,7 +67,8 @@ private static void AddDefaultServices( builder.AddProcessor() .AddProcessor() - .AddProcessor(); + .AddProcessor() + .AddProcessor(); builder.AddConverter>(); builder.AddConverter>(); diff --git a/src/ImageSharp.Web/FormattedImage.cs b/src/ImageSharp.Web/FormattedImage.cs index 7f905a23..ec179a18 100644 --- a/src/ImageSharp.Web/FormattedImage.cs +++ b/src/ImageSharp.Web/FormattedImage.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -11,10 +13,12 @@ namespace SixLabors.ImageSharp.Web /// /// A class encapsulating an image with a particular file encoding. /// - /// + /// public sealed class FormattedImage : IDisposable { + private readonly ImageFormatManager imageFormatsManager; private IImageFormat format; + private IImageEncoder encoder; /// /// Initializes a new instance of the class. @@ -23,8 +27,9 @@ public sealed class FormattedImage : IDisposable /// The format. internal FormattedImage(Image image, IImageFormat format) { - this.format = format; this.Image = image; + this.imageFormatsManager = image.GetConfiguration().ImageFormatsManager; + this.Format = format; } /// @@ -38,7 +43,40 @@ internal FormattedImage(Image image, IImageFormat format) public IImageFormat Format { get => this.format; - set => this.format = value ?? throw new ArgumentNullException(nameof(value)); + set + { + if (value is null) + { + ThrowNull(nameof(value)); + } + + this.format = value; + this.encoder = this.imageFormatsManager.FindEncoder(value); + } + } + + /// + /// Gets or sets the encoder. + /// + public IImageEncoder Encoder + { + get => this.encoder; + set + { + if (value is null) + { + ThrowNull(nameof(value)); + } + + // The given type should match the format encoder. + IImageEncoder reference = this.imageFormatsManager.FindEncoder(this.Format); + if (reference.GetType() != value.GetType()) + { + ThrowInvalid(nameof(value)); + } + + this.encoder = value; + } } /// @@ -54,18 +92,25 @@ public static FormattedImage Load(Configuration configuration, Stream source) } /// - /// Saves the specified destination. + /// Saves image to the specified destination stream. /// - /// The destination. - public void Save(Stream destination) => this.Image.Save(destination, this.format); + /// The destination stream. + public void Save(Stream destination) => this.Image.Save(destination, this.encoder); /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. /// public void Dispose() { this.Image?.Dispose(); this.Image = null; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowNull(string name) => throw new ArgumentNullException(name); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalid(string name) => throw new ArgumentException(name); } } diff --git a/src/ImageSharp.Web/Middleware/ImageProcessingContext.cs b/src/ImageSharp.Web/Middleware/ImageProcessingContext.cs index c73c6062..f96ae9c6 100644 --- a/src/ImageSharp.Web/Middleware/ImageProcessingContext.cs +++ b/src/ImageSharp.Web/Middleware/ImageProcessingContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; @@ -18,7 +18,7 @@ public class ImageProcessingContext /// The current HTTP request context. /// The stream containing the processed image bytes. /// The parsed collection of processing commands. - /// The content type for for the processed image.. + /// The content type for the processed image. /// The file extension for the processed image. public ImageProcessingContext( HttpContext context, @@ -59,4 +59,4 @@ public ImageProcessingContext( /// public string Extension { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp.Web/Processors/FormatWebProcessor.cs b/src/ImageSharp.Web/Processors/FormatWebProcessor.cs index c588fee7..2f78f0ca 100644 --- a/src/ImageSharp.Web/Processors/FormatWebProcessor.cs +++ b/src/ImageSharp.Web/Processors/FormatWebProcessor.cs @@ -56,7 +56,8 @@ public FormattedImage Process( if (!string.IsNullOrWhiteSpace(extension)) { - IImageFormat format = this.options.Configuration.ImageFormatsManager.FindFormatByFileExtension(extension); + IImageFormat format = this.options.Configuration + .ImageFormatsManager.FindFormatByFileExtension(extension); if (format != null) { diff --git a/src/ImageSharp.Web/Processors/JpegQualityWebProcessor.cs b/src/ImageSharp.Web/Processors/JpegQualityWebProcessor.cs new file mode 100644 index 00000000..428924c9 --- /dev/null +++ b/src/ImageSharp.Web/Processors/JpegQualityWebProcessor.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Web.Commands; + +namespace SixLabors.ImageSharp.Web.Processors +{ + /// + /// Allows the setting of quality for the jpeg image format. + /// + public class JpegQualityWebProcessor : IImageWebProcessor + { + /// + /// The command constant for quality. + /// + public const string Quality = "quality"; + + /// + /// The reusable collection of commands. + /// + private static readonly IEnumerable QualityCommands + = new[] { Quality }; + + /// + public IEnumerable Commands { get; } = QualityCommands; + + /// + public FormattedImage Process( + FormattedImage image, + ILogger logger, + IDictionary commands, + CommandParser parser, + CultureInfo culture) + { + if (commands.ContainsKey(Quality) && image.Format is JpegFormat) + { + var reference = + (JpegEncoder)image.Image + .GetConfiguration() + .ImageFormatsManager + .FindEncoder(image.Format); + + // The encoder clamps any values so no validation is required. + int quality = parser.ParseValue(commands.GetValueOrDefault(Quality), culture); + + if (quality != reference.Quality) + { + image.Encoder = new JpegEncoder() { Quality = quality, Subsample = reference.Subsample }; + } + } + + return image; + } + } +} diff --git a/tests/ImageSharp.Web.Tests/DependencyInjection/ServiceRegistrationExtensionsTests.cs b/tests/ImageSharp.Web.Tests/DependencyInjection/ServiceRegistrationExtensionsTests.cs index 1b36bde2..2037fa78 100644 --- a/tests/ImageSharp.Web.Tests/DependencyInjection/ServiceRegistrationExtensionsTests.cs +++ b/tests/ImageSharp.Web.Tests/DependencyInjection/ServiceRegistrationExtensionsTests.cs @@ -28,6 +28,7 @@ public void DefaultServicesAreRegistered() Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(ResizeWebProcessor)); Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(FormatWebProcessor)); Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(BackgroundColorWebProcessor)); + Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(JpegQualityWebProcessor)); Assert.Contains(services, x => x.ServiceType == typeof(CommandParser)); Assert.Contains(services, x => x.ServiceType == typeof(ICommandConverter) && x.ImplementationType == typeof(IntegralNumberConverter)); diff --git a/tests/ImageSharp.Web.Tests/Processors/FormattedImageTests.cs b/tests/ImageSharp.Web.Tests/Processors/FormattedImageTests.cs new file mode 100644 index 00000000..afde3fb0 --- /dev/null +++ b/tests/ImageSharp.Web.Tests/Processors/FormattedImageTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Web.Tests.Processors +{ + public class FormattedImageTests + { + [Fact] + public void ConstructorSetsProperties() + { + using var image = new Image(1, 1); + using var formatted = new FormattedImage(image, JpegFormat.Instance); + + Assert.NotNull(formatted.Image); + Assert.Equal(image, formatted.Image); + + Assert.NotNull(formatted.Format); + Assert.Equal(JpegFormat.Instance, formatted.Format); + + Assert.NotNull(formatted.Encoder); + Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType()); + } + + [Fact] + public void CanSetFormat() + { + using var image = new Image(1, 1); + using var formatted = new FormattedImage(image, JpegFormat.Instance); + + Assert.NotNull(formatted.Format); + Assert.Equal(JpegFormat.Instance, formatted.Format); + + Assert.Throws(() => formatted.Format = null); + + formatted.Format = PngFormat.Instance; + Assert.Equal(PngFormat.Instance, formatted.Format); + Assert.Equal(typeof(PngEncoder), formatted.Encoder.GetType()); + } + + [Fact] + public void CanSetEncoder() + { + using var image = new Image(1, 1); + using var formatted = new FormattedImage(image, PngFormat.Instance); + + Assert.NotNull(formatted.Format); + Assert.Equal(PngFormat.Instance, formatted.Format); + + Assert.Throws(() => formatted.Encoder = null); + Assert.Throws(() => formatted.Encoder = new JpegEncoder()); + + formatted.Format = JpegFormat.Instance; + Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType()); + + JpegSubsample current = ((JpegEncoder)formatted.Encoder).Subsample.GetValueOrDefault(); + + Assert.Equal(JpegSubsample.Ratio444, current); + formatted.Encoder = new JpegEncoder { Subsample = JpegSubsample.Ratio420 }; + + JpegSubsample replacement = ((JpegEncoder)formatted.Encoder).Subsample.GetValueOrDefault(); + + Assert.NotEqual(current, replacement); + Assert.Equal(JpegSubsample.Ratio420, replacement); + } + } +} diff --git a/tests/ImageSharp.Web.Tests/Processors/JpegQualityWebProcessorTests.cs b/tests/ImageSharp.Web.Tests/Processors/JpegQualityWebProcessorTests.cs new file mode 100644 index 00000000..ae503e53 --- /dev/null +++ b/tests/ImageSharp.Web.Tests/Processors/JpegQualityWebProcessorTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Globalization; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Commands.Converters; +using SixLabors.ImageSharp.Web.Processors; +using Xunit; + +namespace SixLabors.ImageSharp.Web.Tests.Processors +{ + public class JpegQualityWebProcessorTests + { + [Fact] + public void JpegQualityWebProcessor_UpdatesQuality() + { + var parser = new CommandParser(new[] { new IntegralNumberConverter() }); + CultureInfo culture = CultureInfo.InvariantCulture; + + var commands = new Dictionary + { + { JpegQualityWebProcessor.Quality, "42" }, + }; + + using var image = new Image(1, 1); + using var formatted = new FormattedImage(image, JpegFormat.Instance); + Assert.Equal(JpegFormat.Instance, formatted.Format); + Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType()); + + new JpegQualityWebProcessor() + .Process(formatted, null, commands, parser, culture); + + Assert.Equal(JpegFormat.Instance, formatted.Format); + Assert.Equal(42, ((JpegEncoder)formatted.Encoder).Quality); + } + } +} diff --git a/tests/ImageSharp.Web.Tests/TestUtilities/AzureBlobStorageCacheTestServerFixture.cs b/tests/ImageSharp.Web.Tests/TestUtilities/AzureBlobStorageCacheTestServerFixture.cs index eac8ddc3..e68ee620 100644 --- a/tests/ImageSharp.Web.Tests/TestUtilities/AzureBlobStorageCacheTestServerFixture.cs +++ b/tests/ImageSharp.Web.Tests/TestUtilities/AzureBlobStorageCacheTestServerFixture.cs @@ -54,6 +54,7 @@ protected override void ConfigureServices(IServiceCollection services) { Assert.NotNull(context); Assert.NotNull(context.Format); + Assert.NotNull(context.Encoder); Assert.NotNull(context.Image); return onBeforeSaveAsync.Invoke(context); diff --git a/tests/ImageSharp.Web.Tests/TestUtilities/PhysicalFileSystemCacheTestServerFixture.cs b/tests/ImageSharp.Web.Tests/TestUtilities/PhysicalFileSystemCacheTestServerFixture.cs index 5ca961d5..f42fde9b 100644 --- a/tests/ImageSharp.Web.Tests/TestUtilities/PhysicalFileSystemCacheTestServerFixture.cs +++ b/tests/ImageSharp.Web.Tests/TestUtilities/PhysicalFileSystemCacheTestServerFixture.cs @@ -52,6 +52,7 @@ protected override void ConfigureServices(IServiceCollection services) { Assert.NotNull(context); Assert.NotNull(context.Format); + Assert.NotNull(context.Encoder); Assert.NotNull(context.Image); return onBeforeSaveAsync.Invoke(context);