Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp.Web
- I have verified if the problem exist in both
DEBUG
andRELEASE
mode - I have searched open and closed issues to ensure it has not already been reported
Description
According to the documentation processing operations should use the order in which they are supplied in the query string, but this is currently not the case. Instead, it loops through all processors in the order they are registered and calls the Process
method on each one (even when none of the commands for that processor are provided):
This probably wasn't noticed before, because the order in which the built-in processors are called doesn't affect the end-result (ResizeWebProcessor
, FormatWebProcessor
, BackgroundColorWebProcessor
and JpegQualityWebProcessor
all affect different aspects of an image). All processors also include a check before mutating the image, so that ensured there were no side-effects by always invoking them.
Umbraco required a custom processor to crop images using coordinates (representing a crop box/rectangle), but adding that after the default built-in processors would result in cropping the already resized image, instead of using the original.
Steps to Reproduce
You can use the CropWebProcessor
from umbraco/Umbraco-CMS#10623 to test this issue (or create a similar simple one):
- Only adding the processor (after the default ones) ends up with wrongly cropped/resized images;
- Removing the
ResizeWebProcessor
, adding the custom processor and then re-adding it again will correctly crop before resizing.
Suggested solution
The ImageSharpMiddleware
already keeps track of all known processing commands, filters unknown ones from the current request and short-circuits when there are none. This would mean only the above Process
extension method will need to be updated.
Besides invoking the processor in the order they are provided in the query string, we need to ensure they aren't invoked for every command (e.g. twice when using ?width=200&height=200
). This can be done by tracking the all commands used by a processor:
var processedCommands = new List<string>();
foreach (KeyValuePair<string, string> command in commands)
{
if (processedCommands.Contains(command.Key, StringComparer.OrdinalIgnoreCase))
{
// Skip commands already used by a processor
continue;
}
foreach (IImageWebProcessor processor in processors.Where(p => p.Commands.Contains(command.Key, StringComparer.OrdinalIgnoreCase)))
{
source = processor.Process(source, logger, commands, commandParser, culture);
processedCommands.AddRange(processor.Commands);
}
}
Or by tracking available processors and remove them after invoking:
var availableProcessors = processors.ToList();
foreach (KeyValuePair<string, string> command in commands)
{
foreach (IImageWebProcessor processor in availableProcessors.Where(p => p.Commands.Contains(command.Key, StringComparer.OrdinalIgnoreCase)).ToList())
{
source = processor.Process(source, logger, commands, commandParser, culture);
// Prevent invoking the processor again
availableProcessors.Remove(processor);
}
}
I haven't tested the above code, but it should give a good start for a PR/fix and might be something to benchmark/compare against each other.
Other notes
The current API (using IDictionary<string, string> commands
) combines duplicate query string keys, making it impossible to run processors more than once using different commands, e.g. resize width to 200, crop and then resize the cropped version to a height of 300 (there are definitely better examples to illustrate the use case).
This might be because QueryHelpers.ParseQuery()
is used, which creates an IDictionary<string, StringValues>
and thereby already loses the order of duplicate query string keys. Because the StringValues
are then converted into a single string (joining multiple values with a comma), you might end up with invalid values (e.g. ?width=100&width=200
will result in 100,200
, which gets interpreted as 100200
when used as resize width)!
Also something to note: the cache key only uses known processor commands, so adding cache busting parameters (e.g. rnd
or v
) doesn't currently work. There is a CacheBusterWebProcessor
, but that's only used/added for tests. Changing the order does though, so that part should already work as expected!
System Configuration
- ImageSharp.Web version: 1.0.3
- Other Six Labors packages and versions: ImageSharp 1.0.3
- Environment (Operating system, version and so on): Windows 10 (21H1 64-bit)
- .NET Framework version: .NET 5
- Additional information: Umbraco 9.0.0-rc001