diff --git a/.editorconfig b/.editorconfig
index 83670fa8..03036f8a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,5 +1,5 @@
-# Version: 1.6.2 (Using https://semver.org/)
-# Updated: 2020-11-02
+# Version: 2.1.0 (Using https://semver.org/)
+# Updated: 2021-03-03
# See https://github.com/RehanSaeed/EditorConfig/releases for release notes.
# See https://github.com/RehanSaeed/EditorConfig for updates to this file.
# See http://EditorConfig.org for more information about .editorconfig files.
@@ -60,87 +60,84 @@ indent_size = 2
[*.{cmd,bat}]
end_of_line = crlf
+# Bash Files
+[*.sh]
+end_of_line = lf
+
# Makefiles
[Makefile]
indent_style = tab
##########################################
-# File Header (Uncomment to support file headers)
-# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
+# Default .NET Code Style Severities
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope
##########################################
-# [*.{cs,csx,cake,vb,vbx,tt,ttinclude}]
-file_header_template = Copyright (c) Six Labors.\nLicensed under the Apache License, Version 2.0.
-
-# SA1636: File header copyright text should match
-# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
-# dotnet_diagnostic.SA1636.severity = none
+[*.{cs,csx,cake,vb,vbx}]
+# Default Severity for all .NET Code Style rules below
+dotnet_analyzer_diagnostic.severity = warning
##########################################
-# .NET Language Conventions
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
+# Language Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules
##########################################
-# .NET Code Style Settings
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings
+# .NET Style Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules
[*.{cs,csx,cake,vb,vbx}]
# "this." and "Me." qualifiers
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me
dotnet_style_qualification_for_field = true:warning
dotnet_style_qualification_for_property = true:warning
dotnet_style_qualification_for_method = true:warning
dotnet_style_qualification_for_event = true:warning
# Language keywords instead of framework type names for type references
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Modifier preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
dotnet_style_require_accessibility_modifiers = always:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
dotnet_style_readonly_field = true:warning
# Parentheses preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion
# Expression-level preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
dotnet_style_object_initializer = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_auto_properties = true:warning
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
+dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
+dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_compound_assignment = true:warning
+dotnet_style_prefer_simplified_interpolation = true:warning
+dotnet_style_prefer_simplified_boolean_expressions = true:warning
# Null-checking preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
-# Parameter preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences
-dotnet_code_quality_unused_parameters = all:warning
-# More style options (Undocumented)
-# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+# File header preferences
+file_header_template = Copyright (c) Six Labors.\nLicensed under the Apache License, Version 2.0.
+# SA1636: File header copyright text should match
+# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
+# dotnet_diagnostic.SA1636.severity = none
+
+# Undocumented
dotnet_style_operator_placement_when_wrapping = end_of_line
-# https://github.com/dotnet/roslyn/pull/40070
-dotnet_style_prefer_simplified_interpolation = true:warning
-# C# Code Style Settings
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
+# C# Style Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
-# Implicit and explicit types
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types
+# 'var' preferences
csharp_style_var_for_built_in_types = never
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = false:warning
# Expression-bodied members
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_constructors = true:warning
csharp_style_expression_bodied_operators = true:warning
@@ -149,47 +146,64 @@ csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_lambdas = true:warning
csharp_style_expression_bodied_local_functions = true:warning
-# Pattern matching
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching
+# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
-# Inlined variable declarations
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations
-csharp_style_inlined_variable_declaration = true:warning
+csharp_style_prefer_switch_expression = true:warning
+csharp_style_prefer_pattern_matching = true:warning
+csharp_style_prefer_not_pattern = true:warning
# Expression-level preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
+csharp_style_inlined_variable_declaration = true:warning
csharp_prefer_simple_default_expression = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
# "Null" checking preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
# Code block preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences
csharp_prefer_braces = true:warning
-# Unused value preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences
-csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-# Index and range preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences
-csharp_style_prefer_index_operator = true:warning
-csharp_style_prefer_range_operator = true:warning
-# Miscellaneous preferences
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences
-csharp_style_deconstructed_variable_declaration = true:warning
-csharp_style_pattern_local_over_anonymous_function = true:warning
+csharp_prefer_simple_using_statement = true:suggestion
+dotnet_diagnostic.IDE0063.severity = suggestion
+# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:warning
+# Modifier preferences
csharp_prefer_static_local_function = true:warning
-csharp_prefer_simple_using_statement = true:suggestion
##########################################
-# .NET Formatting Conventions
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions
+# Unnecessary Code Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules
##########################################
-# Organize usings
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives
+# .NET Unnecessary code rules
+[*.{cs,csx,cake,vb,vbx}]
+dotnet_code_quality_unused_parameters = all:warning
+dotnet_remove_unnecessary_suppression_exclusions = none:warning
+
+# C# Unnecessary code rules
+[*.{cs,csx,cake}]
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+dotnet_diagnostic.IDE0058.severity = suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+dotnet_diagnostic.IDE0059.severity = suggestion
+
+##########################################
+# Formatting Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules
+##########################################
+
+# .NET formatting rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#net-formatting-rules
+[*.{cs,csx,cake,vb,vbx}]
+# Organize using directives
dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# C# formatting rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules
+[*.{cs,csx,cake}]
# Newline options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
csharp_new_line_before_open_brace = all
@@ -231,14 +245,14 @@ csharp_space_around_declaration_statements = false
csharp_space_before_open_square_brackets = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_square_brackets = false
-# Wrapping options
+# Wrap options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
##########################################
-# .NET Naming Conventions
-# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions
+# .NET Naming Rules
+# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/naming-rules
##########################################
[*.{cs,csx,cake,vb,vbx}]
@@ -261,8 +275,9 @@ dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_
dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
# disallowed_style - Anything that has this style applied is marked as disallowed
dotnet_naming_style.disallowed_style.capitalization = pascal_case
-dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
-dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
+# Disabled while we investigate compatibility with VS 16.10
+#dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
+#dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
dotnet_naming_style.internal_error_style.capitalization = pascal_case
dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
diff --git a/.gitattributes b/.gitattributes
index 416dd0d0..70ced690 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -86,7 +86,6 @@
*.dll binary
*.eot binary
*.exe binary
-*.ktx binary
*.otf binary
*.pbm binary
*.pdf binary
@@ -125,3 +124,5 @@
*.tga filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.dds filter=lfs diff=lfs merge=lfs -text
+*.ktx filter=lfs diff=lfs merge=lfs -text
+*.ktx2 filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 5a9d1dde..f4e5702e 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Ask a Question
- url: https://github.com/SixLabors/ImageSharp/discussions/new?category_id=6331980
+ url: https://github.com/SixLabors/ImageSharp.Web/discussions/new?category=q-a
about: Ask a question about this project.
- name: Feature Request
- url: https://github.com/SixLabors/ImageSharp/discussions/new?category_id=6331981
+ url: https://github.com/SixLabors/ImageSharp.Web/discussions/new?category=show-and-tell
about: Share ideas for new features for this project.
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index a05e0f78..97005fd2 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -38,6 +38,20 @@ jobs:
steps:
- uses: actions/checkout@v2
+ # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
+ - name: Create LFS file list
+ run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
+
+ - name: Restore LFS cache
+ uses: actions/cache@v2
+ id: lfs-cache
+ with:
+ path: .git/lfs
+ key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
+
+ - name: Git LFS Pull
+ run: git lfs pull
+
- name: Install NuGet
uses: NuGet/setup-nuget@v1
@@ -63,15 +77,25 @@ jobs:
npm install -g azurite
azurite --loose &
+ - name: Setup NuGet Cache
+ uses: actions/cache@v2
+ id: nuget-cache
+ with:
+ path: ~/.nuget
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
+ restore-keys: ${{ runner.os }}-nuget-
+
- name: Build
shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}"
+ env:
+ SIXLABORS_TESTING: True
- name: Test
shell: pwsh
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
env:
- CI: True
+ SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Web.Tests # Required for xunit
- name: Update Codecov
diff --git a/README.md b/README.md
index de54b4ff..4e382e7b 100644
--- a/README.md
+++ b/README.md
@@ -17,14 +17,20 @@ SixLabors.ImageSharp.Web
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
-### **ImageSharp.Web** is a new high-performance ASP.NET Core middleware leveraging the ImageSharp graphics library.
+### **ImageSharp.Web** is a new high-performance ASP.NET Core middleware leveraging the ImageSharp graphics library to allow on-the-fly image manipulation via URL based commands.
## License
- ImageSharp.Web is licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0)
-- An alternative Commercial License can be purchased for projects and applications requiring support.
+- An alternative Commercial Support License can be purchased **for projects and applications requiring support**.
Please visit https://sixlabors.com/pricing for details.
+## Support Six Labors
+
+Support the efforts of the development of the Six Labors projects.
+ - [Purchase a Commercial Support License :heart:](https://sixlabors.com/pricing/)
+ - [Become a sponsor via GitHub Sponsors :heart:]( https://github.com/sponsors/SixLabors)
+ - [Become a sponsor via Open Collective :heart:](https://opencollective.com/sixlabors)
## Documentation
- [Detailed documentation](https://sixlabors.github.io/docs/) for the ImageSharp.Web API is available. This includes additional conceptual documentation to help you get started.
@@ -106,62 +112,4 @@ Please... Spread the word, contribute algorithms, submit performance improvement
- [Dirk Lemstra](https://github.com/dlemstra)
- [Anton Firsov](https://github.com/antonfirsov)
- [Scott Williams](https://github.com/tocsoft)
-- [Brian Popow](https://github.com/brianpopow)
-
-## Sponsor Six Labors
-
-Support the efforts of the development of the Six Labors projects. [[Become a sponsor :heart:](https://opencollective.com/sixlabors#sponsor)]
-
-### Platinum Sponsors
-Become a platinum sponsor with a monthly donation of $2000 (providing 32 hours of maintenance and development) and get 2 hours of dedicated support (remote support available through chat or screen-sharing) per month.
-
-In addition you get your logo (large) on our README on GitHub and the home page (large) of sixlabors.com
-
-
-
-### Gold Sponsors
-Become a gold sponsor with a monthly donation of $1000 (providing 16 hours of maintenance and development) and get 1 hour of dedicated support (remote support available through chat or screen-sharing) per month.
-
-In addition you get your logo (large) on our README on GitHub and the home page (medium) of sixlabors.com
-
-
-
-
-
-
-
-
-
-
-
-
-
-### Silver Sponsors
-Become a silver sponsor with a monthly donation of $500 (providing 8 hours of maintenance and development) and get your logo (medium) on our README on GitHub and the product pages of sixlabors.com
-
-
-
-
-
-
-
-
-
-
-
-
-
-### Bronze Sponsors
-Become a bronze sponsor with a monthly donation of $100 and get your logo (small) on our README on GitHub.
-
-
-
-
-
-
-
-
-
-
-
-
+- [Brian Popow](https://github.com/brianpopow)
\ No newline at end of file
diff --git a/shared-infrastructure b/shared-infrastructure
index 06a73398..48e73f45 160000
--- a/shared-infrastructure
+++ b/shared-infrastructure
@@ -1 +1 @@
-Subproject commit 06a733983486638b9e38197c7c6eb197ecac43e6
+Subproject commit 48e73f455f15eafefbe3175efc7433e5f277e506
diff --git a/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParser.cs b/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParser.cs
new file mode 100644
index 00000000..67fc51ca
--- /dev/null
+++ b/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParser.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace SixLabors.ImageSharp.Web.Commands
+{
+ ///
+ /// Parses preset name from the request querystring and returns the commands configured for that preset.
+ ///
+ public class PresetOnlyQueryCollectionRequestParser : IRequestParser
+ {
+ private readonly IDictionary> presets;
+
+ ///
+ /// The command constant for the preset query parameter.
+ ///
+ public const string QueryKey = "preset";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The preset options.
+ public PresetOnlyQueryCollectionRequestParser(IOptions presetOptions) =>
+ this.presets = ParsePresets(presetOptions.Value.Presets);
+
+ ///
+ public IDictionary ParseRequestCommands(HttpContext context)
+ {
+ if (context.Request.Query.Count == 0 || !context.Request.Query.ContainsKey(QueryKey))
+ {
+ return new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ var requestedPreset = context.Request.Query["preset"][0];
+ return this.presets.GetValueOrDefault(requestedPreset) ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static IDictionary> ParsePresets(
+ IDictionary unparsedPresets) =>
+ unparsedPresets
+ .Select(keyValue =>
+ new KeyValuePair>(keyValue.Key, ParsePreset(keyValue.Value)))
+ .ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value, StringComparer.OrdinalIgnoreCase);
+
+ private static IDictionary ParsePreset(string unparsedPresetValue)
+ {
+ Dictionary parsed = QueryHelpers.ParseQuery(unparsedPresetValue);
+ var transformed = new Dictionary(parsed.Count, StringComparer.OrdinalIgnoreCase);
+ foreach (KeyValuePair keyValue in parsed)
+ {
+ transformed[keyValue.Key] = keyValue.Value.ToString();
+ }
+
+ return transformed;
+ }
+ }
+}
diff --git a/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParserOptions.cs b/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParserOptions.cs
new file mode 100644
index 00000000..85500d60
--- /dev/null
+++ b/src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParserOptions.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Collections.Generic;
+
+namespace SixLabors.ImageSharp.Web.Commands
+{
+ ///
+ /// Configuration options for the .
+ ///
+ public class PresetOnlyQueryCollectionRequestParserOptions
+ {
+ ///
+ /// Gets or sets the presets, which is a Dictionary of preset names to command query strings.
+ ///
+ public IDictionary Presets { get; set; } = new Dictionary();
+ }
+}
diff --git a/src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs b/src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs
index 34fec826..698879b8 100644
--- a/src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs
+++ b/src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs
@@ -23,7 +23,7 @@ public IDictionary ParseRequestCommands(HttpContext context)
}
Dictionary parsed = QueryHelpers.ParseQuery(context.Request.QueryString.ToUriComponent());
- var transformed = new Dictionary(parsed.Count);
+ var transformed = new Dictionary(parsed.Count, StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair pair in parsed)
{
transformed[pair.Key] = pair.Value.ToString();
diff --git a/src/ImageSharp.Web/DependencyInjection/ImageSharpBuilderExtensions.cs b/src/ImageSharp.Web/DependencyInjection/ImageSharpBuilderExtensions.cs
index 5e4adea3..a0c27d2e 100644
--- a/src/ImageSharp.Web/DependencyInjection/ImageSharpBuilderExtensions.cs
+++ b/src/ImageSharp.Web/DependencyInjection/ImageSharpBuilderExtensions.cs
@@ -54,12 +54,9 @@ public static IImageSharpBuilder SetRequestParser(this IImageSharpBuilder builde
/// The core builder.
/// The factory method for returning a .
/// The .
+ [Obsolete("Use ImageSharp.Configuration.MemoryAllocator. This will be removed in a future version.", true)]
public static IImageSharpBuilder SetMemoryAllocator(this IImageSharpBuilder builder, Func implementationFactory)
- {
- var descriptor = new ServiceDescriptor(typeof(MemoryAllocator), implementationFactory, ServiceLifetime.Singleton);
- builder.Services.Replace(descriptor);
- return builder;
- }
+ => builder;
///
/// Sets the given adding it to the service collection.
@@ -67,13 +64,10 @@ public static IImageSharpBuilder SetMemoryAllocator(this IImageSharpBuilder buil
/// The type of class implementing to add.
/// The core builder.
/// The .
+ [Obsolete("Use ImageSharp.Configuration.MemoryAllocator. This will be removed in a future version.", true)]
public static IImageSharpBuilder SetMemoryAllocator(this IImageSharpBuilder builder)
where TMemoryAllocator : MemoryAllocator
- {
- var descriptor = new ServiceDescriptor(typeof(MemoryAllocator), typeof(TMemoryAllocator), ServiceLifetime.Singleton);
- builder.Services.Replace(descriptor);
- return builder;
- }
+ => builder;
///
/// Sets the given adding it to the service collection.
diff --git a/src/ImageSharp.Web/Middleware/ConcurrentDictionaryExtensions.cs b/src/ImageSharp.Web/Middleware/ConcurrentDictionaryExtensions.cs
new file mode 100644
index 00000000..09b77544
--- /dev/null
+++ b/src/ImageSharp.Web/Middleware/ConcurrentDictionaryExtensions.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Concurrent;
+using System.Threading.Tasks;
+
+namespace SixLabors.ImageSharp.Web.Middleware
+{
+ ///
+ /// Extensions used to manage asynchronous access to the
+ /// https://gist.github.com/davidfowl/3dac8f7b3d141ae87abf770d5781feed
+ ///
+ public static class ConcurrentDictionaryExtensions
+ {
+ ///
+ /// Provides an alternative to specifically for asynchronous values. The factory method will only run once.
+ ///
+ /// The type of the key.
+ /// The value for the dictionary.
+ /// The .
+ /// The key of the element to add.
+ /// The function used to generate a value for the key
+ /// The value for the key. This will be either the existing value for the key if the
+ /// key is already in the dictionary, or the new value for the key as returned by valueFactory
+ /// if the key was not in the dictionary.
+ public static async Task GetOrAddAsync(
+ this ConcurrentDictionary> dictionary,
+ TKey key,
+ Func> valueFactory)
+ {
+ while (true)
+ {
+ if (dictionary.TryGetValue(key, out var task))
+ {
+ return await task;
+ }
+
+ // This is the task that we'll return to all waiters. We'll complete it when the factory is complete
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (dictionary.TryAdd(key, tcs.Task))
+ {
+ try
+ {
+ var value = await valueFactory(key);
+ tcs.TrySetResult(value);
+ return await tcs.Task;
+ }
+ catch (Exception ex)
+ {
+ // Make sure all waiters see the exception
+ tcs.SetException(ex);
+
+ // We remove the entry if the factory failed so it's not a permanent failure
+ // and future gets can retry (this could be a pluggable policy)
+ dictionary.TryRemove(key, out _);
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
index aba0dd3e..6bc2c3e8 100644
--- a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
+++ b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
@@ -32,14 +32,14 @@ public class ImageSharpMiddleware
///
/// The write worker used for limiting identical requests.
///
- private static readonly ConcurrentDictionary> WriteWorkers
- = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+ private static readonly ConcurrentDictionary> WriteWorkers
+ = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
///
/// The read worker used for limiting identical requests.
///
- private static readonly ConcurrentDictionary>>> ReadWorkers
- = new ConcurrentDictionary>>>(StringComparer.OrdinalIgnoreCase);
+ private static readonly ConcurrentDictionary> ReadWorkers
+ = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
///
/// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
@@ -251,30 +251,40 @@ private async Task ProcessRequestAsync(
// Check the cache, if present, not out of date and not requiring and update
// we'll simply serve the file from there.
- (bool newOrUpdated, ImageMetadata sourceImageMetadata) =
- await this.IsNewOrUpdatedAsync(sourceImageResolver, imageContext, key);
+ ImageWorkerResult readResult = default;
+ try
+ {
+ readResult = await this.IsNewOrUpdatedAsync(sourceImageResolver, imageContext, key);
+ }
+ finally
+ {
+ ReadWorkers.TryRemove(key, out Task _);
+ }
- if (!newOrUpdated)
+ if (!readResult.IsNewOrUpdated)
{
+ await this.SendResponseAsync(imageContext, key, readResult.CacheImageMetadata, readResult.Resolver);
return;
}
- // Not cached? Let's get it from the image resolver.
- RecyclableMemoryStream outStream = null;
+ // Not cached, or is updated? Let's get it from the image resolver.
+ var sourceImageMetadata = readResult.SourceImageMetadata;
- // Enter a write lock which locks writing and any reads for the same request.
- // This reduces the overheads of unnecessary processing plus avoids file locks.
- await WriteWorkers.GetOrAdd(
- key,
- _ => new Lazy(
- async () =>
+ // Enter an asynchronous write worker which prevents multiple writes and delays any reads for the same request.
+ // This reduces the overheads of unnecessary processing.
+ try
+ {
+ ImageWorkerResult writeResult = await WriteWorkers.GetOrAddAsync(
+ key,
+ async (key) =>
{
+ RecyclableMemoryStream outStream = null;
try
{
// Prevent a second request from starting a read during write execution.
- if (ReadWorkers.TryGetValue(key, out Lazy> readWork))
+ if (ReadWorkers.TryGetValue(key, out Task readWork))
{
- await readWork.Value;
+ await readWork;
}
ImageCacheMetadata cachedImageMetadata = default;
@@ -334,11 +344,27 @@ await WriteWorkers.GetOrAdd(
// Save the image to the cache and send the response to the caller.
await this.cache.SetAsync(key, outStream, cachedImageMetadata);
- // Remove the resolver from the cache so we always resolve next request
+ // Remove any resolver from the cache so we always resolve next request
// for the same key.
CacheResolverLru.TryRemove(key);
- await this.SendResponseAsync(imageContext, key, cachedImageMetadata, outStream, null);
+ // Place the resolver in the lru cache.
+ (IImageCacheResolver ImageCacheResolver, ImageCacheMetadata ImageCacheMetadata) cachedImage = await
+ CacheResolverLru.GetOrAddAsync(
+ key,
+ async k =>
+ {
+ IImageCacheResolver resolver = await this.cache.GetAsync(k);
+ ImageCacheMetadata metadata = default;
+ if (resolver != null)
+ {
+ metadata = await resolver.GetMetaDataAsync();
+ }
+
+ return (resolver, metadata);
+ });
+
+ return new ImageWorkerResult(cachedImage.ImageCacheMetadata, cachedImage.ImageCacheResolver);
}
catch (Exception ex)
{
@@ -350,9 +376,17 @@ await WriteWorkers.GetOrAdd(
finally
{
await this.StreamDisposeAsync(outStream);
- WriteWorkers.TryRemove(key, out Lazy _);
}
- }, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
+ });
+
+ await this.SendResponseAsync(imageContext, key, writeResult.CacheImageMetadata, writeResult.Resolver);
+ }
+ finally
+ {
+ // As soon as we have sent a response from a writer the result is available from a reader so we remove this task.
+ // Any existing awaiters will continue to await.
+ WriteWorkers.TryRemove(key, out Task _);
+ }
}
private ValueTask StreamDisposeAsync(Stream stream)
@@ -377,85 +411,72 @@ private ValueTask StreamDisposeAsync(Stream stream)
#endif
}
- private async Task> IsNewOrUpdatedAsync(
+ private async Task IsNewOrUpdatedAsync(
IImageResolver sourceImageResolver,
ImageContext imageContext,
string key)
{
- if (WriteWorkers.TryGetValue(key, out Lazy writeWork))
+ // Pause until the write has been completed.
+ if (WriteWorkers.TryGetValue(key, out Task writeWorkResult))
{
- await writeWork.Value;
+ return await writeWorkResult;
}
- if (ReadWorkers.TryGetValue(key, out Lazy> readWork))
- {
- return await readWork.Value;
- }
-
- return await ReadWorkers.GetOrAdd(
+ return await ReadWorkers.GetOrAddAsync(
key,
- _ => new Lazy>>(
- async () =>
+ async (key) =>
{
- try
- {
- // Get the source metadata for processing, storing the result for future checks.
- ImageMetadata sourceImageMetadata = await
- SourceMetadataLru.GetOrAddAsync(
- key,
- _ => sourceImageResolver.GetMetaDataAsync());
-
- // Check to see if the cache contains this image.
- // If not, we return early. No further checks necessary.
- (IImageCacheResolver ImageCacheResolver, ImageCacheMetadata ImageCacheMetadata) cachedImage = await
- CacheResolverLru.GetOrAddAsync(
- key,
- async k =>
+ // Get the source metadata for processing, storing the result for future checks.
+ ImageMetadata sourceImageMetadata = await
+ SourceMetadataLru.GetOrAddAsync(
+ key,
+ _ => sourceImageResolver.GetMetaDataAsync());
+
+ // Check to see if the cache contains this image.
+ // If not, we return early. No further checks necessary.
+ (IImageCacheResolver ImageCacheResolver, ImageCacheMetadata ImageCacheMetadata) cachedImage = await
+ CacheResolverLru.GetOrAddAsync(
+ key,
+ async k =>
+ {
+ IImageCacheResolver resolver = await this.cache.GetAsync(k);
+ ImageCacheMetadata metadata = default;
+ if (resolver != null)
{
- IImageCacheResolver resolver = await this.cache.GetAsync(k);
- ImageCacheMetadata metadata = default;
- if (resolver != null)
- {
- metadata = await resolver.GetMetaDataAsync();
- }
-
- return (resolver, metadata);
- });
+ metadata = await resolver.GetMetaDataAsync();
+ }
- if (cachedImage.ImageCacheResolver is null)
- {
- // Remove the null resolver from the store.
- CacheResolverLru.TryRemove(key);
- return (true, sourceImageMetadata);
- }
+ return (resolver, metadata);
+ });
- // Has the cached image expired?
- // Or has the source image changed since the image was last cached?
- if (cachedImage.ImageCacheMetadata.ContentLength == 0 // Fix for old cache without length property
- || cachedImage.ImageCacheMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
- || cachedImage.ImageCacheMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
- {
- // We want to remove the resolver from the store so that the next check gets the updated file.
- CacheResolverLru.TryRemove(key);
- return (true, sourceImageMetadata);
- }
+ if (cachedImage.ImageCacheResolver is null)
+ {
+ // Remove the null resolver from the store.
+ CacheResolverLru.TryRemove(key);
- // We're pulling the image from the cache.
- await this.SendResponseAsync(imageContext, key, cachedImage.ImageCacheMetadata, null, cachedImage.ImageCacheResolver);
- return (false, sourceImageMetadata);
+ return new ImageWorkerResult(sourceImageMetadata);
}
- finally
+
+ // Has the cached image expired?
+ // Or has the source image changed since the image was last cached?
+ if (cachedImage.ImageCacheMetadata.ContentLength == 0 // Fix for old cache without length property
+ || cachedImage.ImageCacheMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
+ || cachedImage.ImageCacheMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
{
- ReadWorkers.TryRemove(key, out Lazy> _);
+ // We want to remove the resolver from the store so that the next check gets the updated file.
+ CacheResolverLru.TryRemove(key);
+ return new ImageWorkerResult(sourceImageMetadata);
}
- }, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
+
+ // The image is cached. Return the cached image so multiple callers can write a response.
+ return new ImageWorkerResult(sourceImageMetadata, cachedImage.ImageCacheMetadata, cachedImage.ImageCacheResolver);
+ });
}
private async Task SendResponseAsync(
ImageContext imageContext,
string key,
ImageCacheMetadata metadata,
- Stream stream,
IImageCacheResolver cacheResolver)
{
imageContext.ComprehendRequestHeaders(metadata.CacheLastWriteTimeUtc, metadata.ContentLength);
@@ -473,7 +494,11 @@ private async Task SendResponseAsync(
this.logger.LogImageServed(imageContext.GetDisplayUrl(), key);
// When stream is null we're sending from the cache.
- await imageContext.SendAsync(stream ?? await cacheResolver.OpenReadAsync(), metadata);
+ using (var stream = await cacheResolver.OpenReadAsync())
+ {
+ await imageContext.SendAsync(stream, metadata);
+ }
+
return;
case ImageContext.PreconditionState.NotModified:
diff --git a/src/ImageSharp.Web/Middleware/ImageWorkerResult.cs b/src/ImageSharp.Web/Middleware/ImageWorkerResult.cs
new file mode 100644
index 00000000..e7210c20
--- /dev/null
+++ b/src/ImageSharp.Web/Middleware/ImageWorkerResult.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Web.Resolvers;
+
+namespace SixLabors.ImageSharp.Web.Middleware
+{
+ ///
+ /// Provides an asynchronous worker result.
+ ///
+ internal readonly struct ImageWorkerResult
+ {
+ public ImageWorkerResult(ImageMetadata sourceImageMetadata)
+ {
+ this.IsNewOrUpdated = true;
+ this.SourceImageMetadata = sourceImageMetadata;
+ this.CacheImageMetadata = default;
+ this.Resolver = default;
+ }
+
+ public ImageWorkerResult(ImageMetadata sourceImageMetadata, ImageCacheMetadata cacheImageMetadata, IImageCacheResolver resolver)
+ {
+ this.IsNewOrUpdated = false;
+ this.SourceImageMetadata = sourceImageMetadata;
+ this.CacheImageMetadata = cacheImageMetadata;
+ this.Resolver = resolver;
+ }
+
+ public ImageWorkerResult(ImageCacheMetadata cacheImageMetadata, IImageCacheResolver resolver)
+ {
+ this.IsNewOrUpdated = false;
+ this.SourceImageMetadata = default;
+ this.CacheImageMetadata = cacheImageMetadata;
+ this.Resolver = resolver;
+ }
+
+ public bool IsNewOrUpdated { get; }
+
+ public ImageMetadata SourceImageMetadata { get; }
+
+ public ImageCacheMetadata CacheImageMetadata { get; }
+
+ public IImageCacheResolver Resolver { get; }
+ }
+}
diff --git a/tests/ImageSharp.Web.Tests/Commands/PresetOnlyQueryCollectionRequestParserTests.cs b/tests/ImageSharp.Web.Tests/Commands/PresetOnlyQueryCollectionRequestParserTests.cs
new file mode 100644
index 00000000..a8cd8125
--- /dev/null
+++ b/tests/ImageSharp.Web.Tests/Commands/PresetOnlyQueryCollectionRequestParserTests.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Commands;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Web.Tests.Commands
+{
+ public class PresetOnlyQueryCollectionRequestParserTests
+ {
+ [Fact]
+ public void PresetOnlyQueryCollectionRequestParserExtractsCommands()
+ {
+ IDictionary expected = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "width", "400" },
+ { "height", "200" }
+ };
+
+ HttpContext context = CreateHttpContext("?preset=Preset1");
+ IDictionary actual = new PresetOnlyQueryCollectionRequestParser(Options.Create(new PresetOnlyQueryCollectionRequestParserOptions
+ {
+ Presets = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Preset1", "width=400&height=200" }
+ }
+ })).ParseRequestCommands(context);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void PresetOnlyQueryCollectionRequestParserExtractsCommandsWithOtherCasing()
+ {
+ IDictionary expected = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "width", "400" },
+ { "height", "200" }
+ };
+
+ HttpContext context = CreateHttpContext("?PRESET=PRESET1");
+ IDictionary actual = new PresetOnlyQueryCollectionRequestParser(Options.Create(new PresetOnlyQueryCollectionRequestParserOptions
+ {
+ Presets = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Preset1", "width=400&height=200" }
+ }
+ })).ParseRequestCommands(context);
+
+ Assert.Equal(expected, actual);
+ }
+
+
+ [Fact]
+ public void PresetOnlyQueryCollectionRequestParserCommandsWithoutPresetParam()
+ {
+ IDictionary expected = new Dictionary();
+
+ HttpContext context = CreateHttpContext("?test=test");
+ IDictionary actual = new PresetOnlyQueryCollectionRequestParser(Options.Create(new PresetOnlyQueryCollectionRequestParserOptions
+ {
+ Presets = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Preset1", "width=400&height=200" }
+ }
+ })).ParseRequestCommands(context);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void PresetOnlyQueryCollectionRequestParserCommandsWithoutMatchingPreset()
+ {
+ IDictionary expected = new Dictionary();
+
+ HttpContext context = CreateHttpContext("?preset=Preset2");
+ IDictionary actual = new PresetOnlyQueryCollectionRequestParser(Options.Create(new PresetOnlyQueryCollectionRequestParserOptions
+ {
+ Presets = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Preset1", "width=400&height=200" }
+ }
+ })).ParseRequestCommands(context);
+
+ Assert.Equal(expected, actual);
+ }
+
+ private static HttpContext CreateHttpContext(string query)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Path = "/testwebsite.com/image-12345.jpeg";
+ httpContext.Request.QueryString = new QueryString(query);
+ return httpContext;
+ }
+ }
+}
diff --git a/tests/ImageSharp.Web.Tests/Processing/AzureBlobStorageCacheServerTests.cs b/tests/ImageSharp.Web.Tests/Processing/AzureBlobStorageCacheServerTests.cs
index 9ebf660d..5f59ff01 100644
--- a/tests/ImageSharp.Web.Tests/Processing/AzureBlobStorageCacheServerTests.cs
+++ b/tests/ImageSharp.Web.Tests/Processing/AzureBlobStorageCacheServerTests.cs
@@ -40,6 +40,7 @@ public async Task CanProcessMultipleIdenticalQueriesAsync(string url)
using HttpResponseMessage response = await this.HttpClient.GetAsync(url + command);
Assert.NotNull(response);
Assert.True(response.IsSuccessStatusCode);
+ Assert.True(response.Content.Headers.ContentLength > 0);
})).ToArray();
var all = Task.WhenAll(tasks);
diff --git a/tests/ImageSharp.Web.Tests/Processing/PhysicalFileSystemCacheServerTests.cs b/tests/ImageSharp.Web.Tests/Processing/PhysicalFileSystemCacheServerTests.cs
index 4b006a7d..9969584e 100644
--- a/tests/ImageSharp.Web.Tests/Processing/PhysicalFileSystemCacheServerTests.cs
+++ b/tests/ImageSharp.Web.Tests/Processing/PhysicalFileSystemCacheServerTests.cs
@@ -40,6 +40,7 @@ public async Task CanProcessMultipleIdenticalQueriesAsync(string url)
using HttpResponseMessage response = await this.HttpClient.GetAsync(url + command);
Assert.NotNull(response);
Assert.True(response.IsSuccessStatusCode);
+ Assert.True(response.Content.Headers.ContentLength > 0);
})).ToArray();
var all = Task.WhenAll(tasks);