Skip to content

Processing does not use query string parameter order #182

Closed
@ronaldbarendse

Description

@ronaldbarendse

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 and RELEASE 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):

public static FormattedImage Process(
this FormattedImage source,
ILogger logger,
IEnumerable<IImageWebProcessor> processors,
IDictionary<string, string> commands,
CommandParser commandParser,
CultureInfo culture)
{
if (commands.Count != 0)
{
foreach (IImageWebProcessor processor in processors)
{
source = processor.Process(source, logger, commands, commandParser, culture);
}
}
return source;
}

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions