Skip to content

Commit 2c5961e

Browse files
Merge pull request #125 from SixLabors/js/jpeg-quality
Allow the ability to set the quality of jpegs.
2 parents f9d781a + f9ab719 commit 2c5961e

File tree

12 files changed

+266
-14
lines changed

12 files changed

+266
-14
lines changed

samples/ImageSharp.Web.Sample/Startup.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public void ConfigureServices(IServiceCollection services)
5858
.AddProvider<PhysicalFileSystemProvider>()
5959
.AddProcessor<ResizeWebProcessor>()
6060
.AddProcessor<FormatWebProcessor>()
61-
.AddProcessor<BackgroundColorWebProcessor>();
61+
.AddProcessor<BackgroundColorWebProcessor>()
62+
.AddProcessor<JpegQualityWebProcessor>();
6263

6364
// Add the default service and options.
6465
//
@@ -134,7 +135,8 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services
134135
.ClearProcessors()
135136
.AddProcessor<ResizeWebProcessor>()
136137
.AddProcessor<FormatWebProcessor>()
137-
.AddProcessor<BackgroundColorWebProcessor>();
138+
.AddProcessor<BackgroundColorWebProcessor>()
139+
.AddProcessor<JpegQualityWebProcessor>();
138140
}
139141

140142
/// <summary>

samples/ImageSharp.Web.Sample/wwwroot/index.html

+28
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,34 @@ <h2>Format</h2>
128128
</div>
129129
</section>
130130
<hr />
131+
<section>
132+
<h2>Jpeg Quality</h2>
133+
<div>
134+
<p>
135+
<code>imagesharp-logo.png?width=300&format=jpg&quality=100</code>
136+
</p>
137+
<p>
138+
<img src="imagesharp-logo.png?width=300&format=jpg&quality=100" />
139+
</p>
140+
</div>
141+
<div>
142+
<p>
143+
<code>imagesharp-logo.png?width=300&format=jpg&quality=50</code>
144+
</p>
145+
<p>
146+
<img src="imagesharp-logo.png?width=300&format=jpg&quality=50" />
147+
</p>
148+
</div>
149+
<div>
150+
<p>
151+
<code>imagesharp-logo.png?width=300&format=jpg&quality=1</code>
152+
</p>
153+
<p>
154+
<img src="imagesharp-logo.png?width=300&format=jpg&quality=1" />
155+
</p>
156+
</div>
157+
</section>
158+
<hr />
131159
<section>
132160
<h2>Background Color</h2>
133161
<div>

src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ private static void AddDefaultServices(
6767

6868
builder.AddProcessor<ResizeWebProcessor>()
6969
.AddProcessor<FormatWebProcessor>()
70-
.AddProcessor<BackgroundColorWebProcessor>();
70+
.AddProcessor<BackgroundColorWebProcessor>()
71+
.AddProcessor<JpegQualityWebProcessor>();
7172

7273
builder.AddConverter<IntegralNumberConverter<sbyte>>();
7374
builder.AddConverter<IntegralNumberConverter<byte>>();

src/ImageSharp.Web/FormattedImage.cs

+52-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.IO;
6+
using System.Runtime.CompilerServices;
7+
using SixLabors.ImageSharp.Advanced;
68
using SixLabors.ImageSharp.Formats;
79
using SixLabors.ImageSharp.PixelFormats;
810

@@ -11,10 +13,12 @@ namespace SixLabors.ImageSharp.Web
1113
/// <summary>
1214
/// A class encapsulating an image with a particular file encoding.
1315
/// </summary>
14-
/// <seealso cref="IDisposable" />
16+
/// <seealso cref="IDisposable"/>
1517
public sealed class FormattedImage : IDisposable
1618
{
19+
private readonly ImageFormatManager imageFormatsManager;
1720
private IImageFormat format;
21+
private IImageEncoder encoder;
1822

1923
/// <summary>
2024
/// Initializes a new instance of the <see cref="FormattedImage"/> class.
@@ -23,8 +27,9 @@ public sealed class FormattedImage : IDisposable
2327
/// <param name="format">The format.</param>
2428
internal FormattedImage(Image<Rgba32> image, IImageFormat format)
2529
{
26-
this.format = format;
2730
this.Image = image;
31+
this.imageFormatsManager = image.GetConfiguration().ImageFormatsManager;
32+
this.Format = format;
2833
}
2934

3035
/// <summary>
@@ -38,7 +43,40 @@ internal FormattedImage(Image<Rgba32> image, IImageFormat format)
3843
public IImageFormat Format
3944
{
4045
get => this.format;
41-
set => this.format = value ?? throw new ArgumentNullException(nameof(value));
46+
set
47+
{
48+
if (value is null)
49+
{
50+
ThrowNull(nameof(value));
51+
}
52+
53+
this.format = value;
54+
this.encoder = this.imageFormatsManager.FindEncoder(value);
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Gets or sets the encoder.
60+
/// </summary>
61+
public IImageEncoder Encoder
62+
{
63+
get => this.encoder;
64+
set
65+
{
66+
if (value is null)
67+
{
68+
ThrowNull(nameof(value));
69+
}
70+
71+
// The given type should match the format encoder.
72+
IImageEncoder reference = this.imageFormatsManager.FindEncoder(this.Format);
73+
if (reference.GetType() != value.GetType())
74+
{
75+
ThrowInvalid(nameof(value));
76+
}
77+
78+
this.encoder = value;
79+
}
4280
}
4381

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

5694
/// <summary>
57-
/// Saves the specified destination.
95+
/// Saves image to the specified destination stream.
5896
/// </summary>
59-
/// <param name="destination">The destination.</param>
60-
public void Save(Stream destination) => this.Image.Save(destination, this.format);
97+
/// <param name="destination">The destination stream.</param>
98+
public void Save(Stream destination) => this.Image.Save(destination, this.encoder);
6199

62100
/// <summary>
63-
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
101+
/// Performs application-defined tasks associated with freeing, releasing, or resetting
102+
/// unmanaged resources.
64103
/// </summary>
65104
public void Dispose()
66105
{
67106
this.Image?.Dispose();
68107
this.Image = null;
69108
}
109+
110+
[MethodImpl(MethodImplOptions.NoInlining)]
111+
private static void ThrowNull(string name) => throw new ArgumentNullException(name);
112+
113+
[MethodImpl(MethodImplOptions.NoInlining)]
114+
private static void ThrowInvalid(string name) => throw new ArgumentException(name);
70115
}
71116
}

src/ImageSharp.Web/Middleware/ImageProcessingContext.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System.Collections.Generic;
@@ -18,7 +18,7 @@ public class ImageProcessingContext
1818
/// <param name="context">The current HTTP request context.</param>
1919
/// <param name="stream">The stream containing the processed image bytes.</param>
2020
/// <param name="commands">The parsed collection of processing commands.</param>
21-
/// <param name="contentType">The content type for for the processed image..</param>
21+
/// <param name="contentType">The content type for the processed image.</param>
2222
/// <param name="extension">The file extension for the processed image.</param>
2323
public ImageProcessingContext(
2424
HttpContext context,
@@ -59,4 +59,4 @@ public ImageProcessingContext(
5959
/// </summary>
6060
public string Extension { get; }
6161
}
62-
}
62+
}

src/ImageSharp.Web/Processors/FormatWebProcessor.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public FormattedImage Process(
5656

5757
if (!string.IsNullOrWhiteSpace(extension))
5858
{
59-
IImageFormat format = this.options.Configuration.ImageFormatsManager.FindFormatByFileExtension(extension);
59+
IImageFormat format = this.options.Configuration
60+
.ImageFormatsManager.FindFormatByFileExtension(extension);
6061

6162
if (format != null)
6263
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using Microsoft.Extensions.Logging;
7+
using SixLabors.ImageSharp.Advanced;
8+
using SixLabors.ImageSharp.Formats.Jpeg;
9+
using SixLabors.ImageSharp.Web.Commands;
10+
11+
namespace SixLabors.ImageSharp.Web.Processors
12+
{
13+
/// <summary>
14+
/// Allows the setting of quality for the jpeg image format.
15+
/// </summary>
16+
public class JpegQualityWebProcessor : IImageWebProcessor
17+
{
18+
/// <summary>
19+
/// The command constant for quality.
20+
/// </summary>
21+
public const string Quality = "quality";
22+
23+
/// <summary>
24+
/// The reusable collection of commands.
25+
/// </summary>
26+
private static readonly IEnumerable<string> QualityCommands
27+
= new[] { Quality };
28+
29+
/// <inheritdoc/>
30+
public IEnumerable<string> Commands { get; } = QualityCommands;
31+
32+
/// <inheritdoc/>
33+
public FormattedImage Process(
34+
FormattedImage image,
35+
ILogger logger,
36+
IDictionary<string, string> commands,
37+
CommandParser parser,
38+
CultureInfo culture)
39+
{
40+
if (commands.ContainsKey(Quality) && image.Format is JpegFormat)
41+
{
42+
var reference =
43+
(JpegEncoder)image.Image
44+
.GetConfiguration()
45+
.ImageFormatsManager
46+
.FindEncoder(image.Format);
47+
48+
// The encoder clamps any values so no validation is required.
49+
int quality = parser.ParseValue<int>(commands.GetValueOrDefault(Quality), culture);
50+
51+
if (quality != reference.Quality)
52+
{
53+
image.Encoder = new JpegEncoder() { Quality = quality, Subsample = reference.Subsample };
54+
}
55+
}
56+
57+
return image;
58+
}
59+
}
60+
}

tests/ImageSharp.Web.Tests/DependencyInjection/ServiceRegistrationExtensionsTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public void DefaultServicesAreRegistered()
2828
Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(ResizeWebProcessor));
2929
Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(FormatWebProcessor));
3030
Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(BackgroundColorWebProcessor));
31+
Assert.Contains(services, x => x.ServiceType == typeof(IImageWebProcessor) && x.ImplementationType == typeof(JpegQualityWebProcessor));
3132
Assert.Contains(services, x => x.ServiceType == typeof(CommandParser));
3233

3334
Assert.Contains(services, x => x.ServiceType == typeof(ICommandConverter) && x.ImplementationType == typeof(IntegralNumberConverter<sbyte>));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using SixLabors.ImageSharp.Formats.Jpeg;
6+
using SixLabors.ImageSharp.Formats.Png;
7+
using SixLabors.ImageSharp.PixelFormats;
8+
using Xunit;
9+
10+
namespace SixLabors.ImageSharp.Web.Tests.Processors
11+
{
12+
public class FormattedImageTests
13+
{
14+
[Fact]
15+
public void ConstructorSetsProperties()
16+
{
17+
using var image = new Image<Rgba32>(1, 1);
18+
using var formatted = new FormattedImage(image, JpegFormat.Instance);
19+
20+
Assert.NotNull(formatted.Image);
21+
Assert.Equal(image, formatted.Image);
22+
23+
Assert.NotNull(formatted.Format);
24+
Assert.Equal(JpegFormat.Instance, formatted.Format);
25+
26+
Assert.NotNull(formatted.Encoder);
27+
Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType());
28+
}
29+
30+
[Fact]
31+
public void CanSetFormat()
32+
{
33+
using var image = new Image<Rgba32>(1, 1);
34+
using var formatted = new FormattedImage(image, JpegFormat.Instance);
35+
36+
Assert.NotNull(formatted.Format);
37+
Assert.Equal(JpegFormat.Instance, formatted.Format);
38+
39+
Assert.Throws<ArgumentNullException>(() => formatted.Format = null);
40+
41+
formatted.Format = PngFormat.Instance;
42+
Assert.Equal(PngFormat.Instance, formatted.Format);
43+
Assert.Equal(typeof(PngEncoder), formatted.Encoder.GetType());
44+
}
45+
46+
[Fact]
47+
public void CanSetEncoder()
48+
{
49+
using var image = new Image<Rgba32>(1, 1);
50+
using var formatted = new FormattedImage(image, PngFormat.Instance);
51+
52+
Assert.NotNull(formatted.Format);
53+
Assert.Equal(PngFormat.Instance, formatted.Format);
54+
55+
Assert.Throws<ArgumentNullException>(() => formatted.Encoder = null);
56+
Assert.Throws<ArgumentException>(() => formatted.Encoder = new JpegEncoder());
57+
58+
formatted.Format = JpegFormat.Instance;
59+
Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType());
60+
61+
JpegSubsample current = ((JpegEncoder)formatted.Encoder).Subsample.GetValueOrDefault();
62+
63+
Assert.Equal(JpegSubsample.Ratio444, current);
64+
formatted.Encoder = new JpegEncoder { Subsample = JpegSubsample.Ratio420 };
65+
66+
JpegSubsample replacement = ((JpegEncoder)formatted.Encoder).Subsample.GetValueOrDefault();
67+
68+
Assert.NotEqual(current, replacement);
69+
Assert.Equal(JpegSubsample.Ratio420, replacement);
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using SixLabors.ImageSharp.Formats.Jpeg;
7+
using SixLabors.ImageSharp.PixelFormats;
8+
using SixLabors.ImageSharp.Web.Commands;
9+
using SixLabors.ImageSharp.Web.Commands.Converters;
10+
using SixLabors.ImageSharp.Web.Processors;
11+
using Xunit;
12+
13+
namespace SixLabors.ImageSharp.Web.Tests.Processors
14+
{
15+
public class JpegQualityWebProcessorTests
16+
{
17+
[Fact]
18+
public void JpegQualityWebProcessor_UpdatesQuality()
19+
{
20+
var parser = new CommandParser(new[] { new IntegralNumberConverter<int>() });
21+
CultureInfo culture = CultureInfo.InvariantCulture;
22+
23+
var commands = new Dictionary<string, string>
24+
{
25+
{ JpegQualityWebProcessor.Quality, "42" },
26+
};
27+
28+
using var image = new Image<Rgba32>(1, 1);
29+
using var formatted = new FormattedImage(image, JpegFormat.Instance);
30+
Assert.Equal(JpegFormat.Instance, formatted.Format);
31+
Assert.Equal(typeof(JpegEncoder), formatted.Encoder.GetType());
32+
33+
new JpegQualityWebProcessor()
34+
.Process(formatted, null, commands, parser, culture);
35+
36+
Assert.Equal(JpegFormat.Instance, formatted.Format);
37+
Assert.Equal(42, ((JpegEncoder)formatted.Encoder).Quality);
38+
}
39+
}
40+
}

tests/ImageSharp.Web.Tests/TestUtilities/AzureBlobStorageCacheTestServerFixture.cs

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ protected override void ConfigureServices(IServiceCollection services)
5454
{
5555
Assert.NotNull(context);
5656
Assert.NotNull(context.Format);
57+
Assert.NotNull(context.Encoder);
5758
Assert.NotNull(context.Image);
5859

5960
return onBeforeSaveAsync.Invoke(context);

0 commit comments

Comments
 (0)