Skip to content

Commit

Permalink
Merge pull request #125 from SixLabors/js/jpeg-quality
Browse files Browse the repository at this point in the history
Allow the ability to set the quality of jpegs.
  • Loading branch information
JimBobSquarePants authored Sep 18, 2020
2 parents f9d781a + f9ab719 commit 2c5961e
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 14 deletions.
6 changes: 4 additions & 2 deletions samples/ImageSharp.Web.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public void ConfigureServices(IServiceCollection services)
.AddProvider<PhysicalFileSystemProvider>()
.AddProcessor<ResizeWebProcessor>()
.AddProcessor<FormatWebProcessor>()
.AddProcessor<BackgroundColorWebProcessor>();
.AddProcessor<BackgroundColorWebProcessor>()
.AddProcessor<JpegQualityWebProcessor>();

// Add the default service and options.
//
Expand Down Expand Up @@ -134,7 +135,8 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services
.ClearProcessors()
.AddProcessor<ResizeWebProcessor>()
.AddProcessor<FormatWebProcessor>()
.AddProcessor<BackgroundColorWebProcessor>();
.AddProcessor<BackgroundColorWebProcessor>()
.AddProcessor<JpegQualityWebProcessor>();
}

/// <summary>
Expand Down
28 changes: 28 additions & 0 deletions samples/ImageSharp.Web.Sample/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ <h2>Format</h2>
</div>
</section>
<hr />
<section>
<h2>Jpeg Quality</h2>
<div>
<p>
<code>imagesharp-logo.png?width=300&format=jpg&quality=100</code>
</p>
<p>
<img src="imagesharp-logo.png?width=300&format=jpg&quality=100" />
</p>
</div>
<div>
<p>
<code>imagesharp-logo.png?width=300&format=jpg&quality=50</code>
</p>
<p>
<img src="imagesharp-logo.png?width=300&format=jpg&quality=50" />
</p>
</div>
<div>
<p>
<code>imagesharp-logo.png?width=300&format=jpg&quality=1</code>
</p>
<p>
<img src="imagesharp-logo.png?width=300&format=jpg&quality=1" />
</p>
</div>
</section>
<hr />
<section>
<h2>Background Color</h2>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ private static void AddDefaultServices(

builder.AddProcessor<ResizeWebProcessor>()
.AddProcessor<FormatWebProcessor>()
.AddProcessor<BackgroundColorWebProcessor>();
.AddProcessor<BackgroundColorWebProcessor>()
.AddProcessor<JpegQualityWebProcessor>();

builder.AddConverter<IntegralNumberConverter<sbyte>>();
builder.AddConverter<IntegralNumberConverter<byte>>();
Expand Down
59 changes: 52 additions & 7 deletions src/ImageSharp.Web/FormattedImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -11,10 +13,12 @@ namespace SixLabors.ImageSharp.Web
/// <summary>
/// A class encapsulating an image with a particular file encoding.
/// </summary>
/// <seealso cref="IDisposable" />
/// <seealso cref="IDisposable"/>
public sealed class FormattedImage : IDisposable
{
private readonly ImageFormatManager imageFormatsManager;
private IImageFormat format;
private IImageEncoder encoder;

/// <summary>
/// Initializes a new instance of the <see cref="FormattedImage"/> class.
Expand All @@ -23,8 +27,9 @@ public sealed class FormattedImage : IDisposable
/// <param name="format">The format.</param>
internal FormattedImage(Image<Rgba32> image, IImageFormat format)
{
this.format = format;
this.Image = image;
this.imageFormatsManager = image.GetConfiguration().ImageFormatsManager;
this.Format = format;
}

/// <summary>
Expand All @@ -38,7 +43,40 @@ internal FormattedImage(Image<Rgba32> 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);
}
}

/// <summary>
/// Gets or sets the encoder.
/// </summary>
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;
}
}

/// <summary>
Expand All @@ -54,18 +92,25 @@ public static FormattedImage Load(Configuration configuration, Stream source)
}

/// <summary>
/// Saves the specified destination.
/// Saves image to the specified destination stream.
/// </summary>
/// <param name="destination">The destination.</param>
public void Save(Stream destination) => this.Image.Save(destination, this.format);
/// <param name="destination">The destination stream.</param>
public void Save(Stream destination) => this.Image.Save(destination, this.encoder);

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// Performs application-defined tasks associated with freeing, releasing, or resetting
/// unmanaged resources.
/// </summary>
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);
}
}
6 changes: 3 additions & 3 deletions src/ImageSharp.Web/Middleware/ImageProcessingContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System.Collections.Generic;
Expand All @@ -18,7 +18,7 @@ public class ImageProcessingContext
/// <param name="context">The current HTTP request context.</param>
/// <param name="stream">The stream containing the processed image bytes.</param>
/// <param name="commands">The parsed collection of processing commands.</param>
/// <param name="contentType">The content type for for the processed image..</param>
/// <param name="contentType">The content type for the processed image.</param>
/// <param name="extension">The file extension for the processed image.</param>
public ImageProcessingContext(
HttpContext context,
Expand Down Expand Up @@ -59,4 +59,4 @@ public ImageProcessingContext(
/// </summary>
public string Extension { get; }
}
}
}
3 changes: 2 additions & 1 deletion src/ImageSharp.Web/Processors/FormatWebProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
60 changes: 60 additions & 0 deletions src/ImageSharp.Web/Processors/JpegQualityWebProcessor.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Allows the setting of quality for the jpeg image format.
/// </summary>
public class JpegQualityWebProcessor : IImageWebProcessor
{
/// <summary>
/// The command constant for quality.
/// </summary>
public const string Quality = "quality";

/// <summary>
/// The reusable collection of commands.
/// </summary>
private static readonly IEnumerable<string> QualityCommands
= new[] { Quality };

/// <inheritdoc/>
public IEnumerable<string> Commands { get; } = QualityCommands;

/// <inheritdoc/>
public FormattedImage Process(
FormattedImage image,
ILogger logger,
IDictionary<string, string> 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<int>(commands.GetValueOrDefault(Quality), culture);

if (quality != reference.Quality)
{
image.Encoder = new JpegEncoder() { Quality = quality, Subsample = reference.Subsample };
}
}

return image;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<sbyte>));
Expand Down
72 changes: 72 additions & 0 deletions tests/ImageSharp.Web.Tests/Processors/FormattedImageTests.cs
Original file line number Diff line number Diff line change
@@ -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<Rgba32>(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<Rgba32>(1, 1);
using var formatted = new FormattedImage(image, JpegFormat.Instance);

Assert.NotNull(formatted.Format);
Assert.Equal(JpegFormat.Instance, formatted.Format);

Assert.Throws<ArgumentNullException>(() => 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<Rgba32>(1, 1);
using var formatted = new FormattedImage(image, PngFormat.Instance);

Assert.NotNull(formatted.Format);
Assert.Equal(PngFormat.Instance, formatted.Format);

Assert.Throws<ArgumentNullException>(() => formatted.Encoder = null);
Assert.Throws<ArgumentException>(() => 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<int>() });
CultureInfo culture = CultureInfo.InvariantCulture;

var commands = new Dictionary<string, string>
{
{ JpegQualityWebProcessor.Quality, "42" },
};

using var image = new Image<Rgba32>(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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 2c5961e

Please sign in to comment.