diff --git a/SixLabors.ImageSharp.Web.props b/SixLabors.ImageSharp.Web.props
new file mode 100644
index 00000000..2d95d7d1
--- /dev/null
+++ b/SixLabors.ImageSharp.Web.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj b/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
index 9c2462b4..d20146da 100644
--- a/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
+++ b/samples/ImageSharp.Web.Sample/ImageSharp.Web.Sample.csproj
@@ -2,9 +2,15 @@
net6.0
+ enable
+ enable
+ 10
+
+
+
diff --git a/samples/ImageSharp.Web.Sample/Pages/Index.cshtml b/samples/ImageSharp.Web.Sample/Pages/Index.cshtml
new file mode 100644
index 00000000..c492abf4
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/Index.cshtml
@@ -0,0 +1,1237 @@
+@page
+@model IndexModel
+@{
+ ViewData["Title"] = "ImageSharp URI API Samples";
+}
+
+@section Css {
+
+}
+
+ Unmodified
+
+
+ sixlabors.imagesharp.web.svg
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.svg?width=300
+
+
+
+
+
+
+
+ No Commands
+
+
+ sixlabors.imagesharp.web.png
+
+
+
+
+
+
+
+ Resize
+
+
+ sixlabors.imagesharp.web.png?width=300
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&height=200
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&height=200&rmode=stretch
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&height=200&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&rsampler=lanczos3
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&rsampler=nearestneighbor
+
+
+
+
+
+
+
+
+ Format
+
+
+ sixlabors.imagesharp.web.png?width=300&format=jpg
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=bmp
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=gif
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=webp
+
+
+
+
+
+
+
+
+ Jpeg Quality
+
+
+ sixlabors.imagesharp.web.png?width=300&format=jpg&quality=100
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=jpg&quality=50
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=jpg&quality=1
+
+
+
+
+
+
+
+
+ Webp Quality
+
+
+ sixlabors.imagesharp.web.png?width=300&format=webp&quality=100
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=webp&quality=50
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&format=webp&quality=1
+
+
+
+
+
+
+
+
+ Background Color
+
+
+ sixlabors.imagesharp.web.png?width=300&bgcolor=red
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&bgcolor=FFFF00
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&bgcolor=128,28,32
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&bgcolor=C1FF0080
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=300&bgcolor=128,28,32,128
+
+
+
+
+
+
+
+
+ Identical Queries
+ Demonstrates that the middleware handles identical queries without file contention.
+
+
+ sixlabors.imagesharp.web.png?width=123
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=123
+
+
+
+
+
+
+
+ sixlabors.imagesharp.web.png?width=123
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&ranchor=bottomleft
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling Portrait
+ Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
+
+
+ Portrait_0.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_1.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_2.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_3.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_4.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_5.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Portrait_6.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_7.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Portrait_8.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&rxy=0.69,0.37
+
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&ranchor=topright
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ EXIF Handling : Landscape
+ Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
+
+
+ Landscape_0.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_1.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_2.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_3.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_4.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_5.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+
+ Landscape_6.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_7.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
+
+ Landscape_8.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
+
+
+
+
+
+
diff --git a/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs b/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs
new file mode 100644
index 00000000..fd22ab50
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/IndexModel.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace ImageSharp.Web.Sample.Pages
+{
+ ///
+ /// Defines the index page view model.
+ ///
+ public class IndexModel : PageModel
+ {
+ }
+}
diff --git a/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml b/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml
new file mode 100644
index 00000000..ccf41dbb
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ @ViewData["Title"] - ImageSharp.Web.Sample
+ @await RenderSectionAsync("Css", required: false)
+
+
+
+
+ @RenderBody()
+
+
+
+
diff --git a/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml.css b/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml.css
new file mode 100644
index 00000000..a72cbeaf
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/Shared/_Layout.cshtml.css
@@ -0,0 +1,48 @@
+/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
+for details on configuring this project to bundle and minify static web assets. */
+
+a.navbar-brand {
+ white-space: normal;
+ text-align: center;
+ word-break: break-all;
+}
+
+a {
+ color: #0077cc;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.border-top {
+ border-top: 1px solid #e5e5e5;
+}
+.border-bottom {
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.box-shadow {
+ box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
+}
+
+button.accept-policy {
+ font-size: 1rem;
+ line-height: inherit;
+}
+
+.footer {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ white-space: nowrap;
+ line-height: 60px;
+}
diff --git a/samples/ImageSharp.Web.Sample/Pages/_ViewImports.cshtml b/samples/ImageSharp.Web.Sample/Pages/_ViewImports.cshtml
new file mode 100644
index 00000000..f5dfabe4
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/_ViewImports.cshtml
@@ -0,0 +1,4 @@
+@using ImageSharp.Web.Sample
+@namespace ImageSharp.Web.Sample.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@addTagHelper *, SixLabors.ImageSharp.Web
diff --git a/samples/ImageSharp.Web.Sample/Pages/_ViewStart.cshtml b/samples/ImageSharp.Web.Sample/Pages/_ViewStart.cshtml
new file mode 100644
index 00000000..a5f10045
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/Pages/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "_Layout";
+}
diff --git a/samples/ImageSharp.Web.Sample/Program.cs b/samples/ImageSharp.Web.Sample/Program.cs
index d691ebeb..44a5de87 100644
--- a/samples/ImageSharp.Web.Sample/Program.cs
+++ b/samples/ImageSharp.Web.Sample/Program.cs
@@ -1,7 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Web.Sample;
+using SixLabors.ImageSharp.Web.Caching;
+using SixLabors.ImageSharp.Web.Commands;
+using SixLabors.ImageSharp.Web.DependencyInjection;
+using SixLabors.ImageSharp.Web.Processors;
+using SixLabors.ImageSharp.Web.Providers;
+
+namespace ImageSharp.Web.Sample;
///
/// The running application.
@@ -9,17 +15,60 @@ namespace SixLabors.ImageSharp.Web.Sample;
public static class Program
{
///
- /// The main entry point to the running application.
+ /// The main application entry point.
///
- /// Any arguments to pass to the application.
- public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
+ /// Argument paramateres.
+ public static void Main(string[] args)
+ {
+ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
- ///
- /// Creates an instance used to create a configured .
- ///
- /// Any arguments to pass to the application.
- /// The .
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup());
+ // Add services to the container.
+ IServiceCollection services = builder.Services;
+ services.AddRazorPages();
+
+ // TODO: Enable HMAC
+ services.AddImageSharp(options => options.HMACSecretKey = new byte[] { 1, 2, 3, 4, 5 })
+ .SetRequestParser()
+ .Configure(options =>
+ {
+ options.CacheRootPath = null;
+ options.CacheFolder = "is-cache";
+ options.CacheFolderDepth = 8;
+ })
+ .SetCache()
+ .SetCacheKey()
+ .SetCacheHash()
+ .Configure(options => options.ProviderRootPath = null)
+ .AddProvider()
+ .AddProcessor()
+ .AddProcessor()
+ .AddProcessor()
+ .AddProcessor()
+ .AddProcessor();
+
+ WebApplication app = builder.Build();
+
+ // Configure the HTTP request pipeline.
+ if (!app.Environment.IsDevelopment())
+ {
+ app.UseExceptionHandler("/Error");
+
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+ }
+
+ app.UseHttpsRedirection();
+
+ app.UseImageSharp();
+
+ app.UseStaticFiles();
+
+ app.UseRouting();
+
+ app.UseAuthorization();
+
+ app.MapRazorPages();
+
+ app.Run();
+ }
}
diff --git a/samples/ImageSharp.Web.Sample/Properties/launchSettings.json b/samples/ImageSharp.Web.Sample/Properties/launchSettings.json
index f4f5915d..a8c03df5 100644
--- a/samples/ImageSharp.Web.Sample/Properties/launchSettings.json
+++ b/samples/ImageSharp.Web.Sample/Properties/launchSettings.json
@@ -3,25 +3,26 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:2645/",
- "sslPort": 0
+ "applicationUrl": "http://localhost:58534",
+ "sslPort": 44388
}
},
"profiles": {
- "IIS Express": {
- "commandName": "IISExpress",
+ "ImageSharp.Web.Sample": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
"launchBrowser": true,
+ "applicationUrl": "https://localhost:7267;http://localhost:5267",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
- "ImageSharp.Web.Sample": {
- "commandName": "Project",
+ "IIS Express": {
+ "commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
- },
- "applicationUrl": "http://localhost:2646"
+ }
}
}
}
diff --git a/samples/ImageSharp.Web.Sample/Startup.cs b/samples/ImageSharp.Web.Sample/Startup.cs
deleted file mode 100644
index a01e8c07..00000000
--- a/samples/ImageSharp.Web.Sample/Startup.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Web.Caching;
-using SixLabors.ImageSharp.Web.Commands;
-using SixLabors.ImageSharp.Web.DependencyInjection;
-using SixLabors.ImageSharp.Web.Processors;
-using SixLabors.ImageSharp.Web.Providers;
-
-namespace SixLabors.ImageSharp.Web.Sample;
-
-///
-/// Contains application configuration allowing the addition of services to the container.
-///
-public class Startup
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The configuration properties.
- public Startup(IConfiguration configuration) => this.AppConfiguration = configuration;
-
- ///
- /// Gets the configuration properties.
- ///
- public IConfiguration AppConfiguration { get; }
-
- ///
- /// This method gets called by the runtime. Use this method to add services to the container.
- ///
- /// The collection of service descriptors.
- public void ConfigureServices(IServiceCollection services)
- => services.AddImageSharp()
- .SetRequestParser()
- .Configure(options =>
- {
- options.CacheRootPath = null;
- options.CacheFolder = "is-cache";
- options.CacheFolderDepth = 8;
- })
- .SetCache()
- .SetCacheKey()
- .SetCacheHash()
- .Configure(options => options.ProviderRootPath = null)
- .AddProvider()
- .AddProcessor()
- .AddProcessor()
- .AddProcessor()
- .AddProcessor()
- .AddProcessor();
-
- // Add the default service and options.
- //
- // services.AddImageSharp();
-
- // Or add the default service and custom options.
- //
- // this.ConfigureDefaultServicesAndCustomOptions(services);
-
- // Or we can fine-grain control adding the default options and configure all other services.
- //
- // this.ConfigureCustomServicesAndDefaultOptions(services);
-
- // Or we can fine-grain control adding custom options and configure all other services
- // There are also factory methods for each builder that will allow building from configuration files.
- //
- // this.ConfigureCustomServicesAndCustomOptions(services);
-
- private void ConfigureDefaultServicesAndCustomOptions(IServiceCollection services)
- => services.AddImageSharp(options =>
- {
- options.Configuration = Configuration.Default;
- options.BrowserMaxAge = TimeSpan.FromDays(7);
- options.CacheMaxAge = TimeSpan.FromDays(365);
- options.CacheHashLength = 8;
- options.OnParseCommandsAsync = _ => Task.CompletedTask;
- options.OnBeforeSaveAsync = _ => Task.CompletedTask;
- options.OnProcessedAsync = _ => Task.CompletedTask;
- options.OnPrepareResponseAsync = _ => Task.CompletedTask;
- });
-
- private void ConfigureCustomServicesAndDefaultOptions(IServiceCollection services)
- => services.AddImageSharp()
- .RemoveProcessor()
- .RemoveProcessor();
-
- private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services)
- => services.AddImageSharp(options =>
- {
- options.Configuration = Configuration.Default;
- options.BrowserMaxAge = TimeSpan.FromDays(7);
- options.CacheMaxAge = TimeSpan.FromDays(365);
- options.CacheHashLength = 8;
- options.OnParseCommandsAsync = _ => Task.CompletedTask;
- options.OnBeforeSaveAsync = _ => Task.CompletedTask;
- options.OnProcessedAsync = _ => Task.CompletedTask;
- options.OnPrepareResponseAsync = _ => Task.CompletedTask;
- })
- .SetRequestParser()
- .Configure(options => options.CacheFolder = "different-cache")
- .SetCache()
- .SetCacheKey()
- .SetCacheHash()
- .ClearProviders()
- .AddProvider()
- .ClearProcessors()
- .AddProcessor()
- .AddProcessor()
- .AddProcessor()
- .AddProcessor();
-
- ///
- /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- ///
- /// The application builder.
- /// The hosting environment the application is running in.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
-
- app.UseDefaultFiles();
- app.UseImageSharp();
- app.UseStaticFiles();
- }
-}
diff --git a/samples/ImageSharp.Web.Sample/appsettings.Development.json b/samples/ImageSharp.Web.Sample/appsettings.Development.json
index e203e940..770d3e93 100644
--- a/samples/ImageSharp.Web.Sample/appsettings.Development.json
+++ b/samples/ImageSharp.Web.Sample/appsettings.Development.json
@@ -1,9 +1,9 @@
{
+ "DetailedErrors": true,
"Logging": {
"LogLevel": {
- "Default": "Debug",
- "System": "Information",
- "Microsoft": "Information"
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
}
}
}
diff --git a/samples/ImageSharp.Web.Sample/appsettings.json b/samples/ImageSharp.Web.Sample/appsettings.json
index 7cb5ac81..10f68b8c 100644
--- a/samples/ImageSharp.Web.Sample/appsettings.json
+++ b/samples/ImageSharp.Web.Sample/appsettings.json
@@ -1,8 +1,8 @@
{
"Logging": {
"LogLevel": {
- "Default": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
diff --git a/samples/ImageSharp.Web.Sample/wwwroot/favicon.ico b/samples/ImageSharp.Web.Sample/wwwroot/favicon.ico
new file mode 100644
index 00000000..63e859b4
Binary files /dev/null and b/samples/ImageSharp.Web.Sample/wwwroot/favicon.ico differ
diff --git a/samples/ImageSharp.Web.Sample/wwwroot/index.html b/samples/ImageSharp.Web.Sample/wwwroot/index.html
deleted file mode 100644
index e247c7f7..00000000
--- a/samples/ImageSharp.Web.Sample/wwwroot/index.html
+++ /dev/null
@@ -1,1004 +0,0 @@
-
-
-
-
-
-
-
-
- ImageSharp URI API Samples
-
-
- Unmodified
-
-
- sixlabors.imagesharp.web.png
-
-
-
-
-
-
-
- Resize
-
-
- sixlabors.imagesharp.web.png?width=300
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&height=200
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&height=200&rmode=stretch
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&height=200&rmode=pad&rcolor=limegreen
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&rsampler=lanczos3
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&rsampler=nearest
-
-
-
-
-
-
-
-
- Format
-
-
- sixlabors.imagesharp.web.png?width=300&format=jpg
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=bmp
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=gif
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=webp
-
-
-
-
-
-
-
-
- Jpeg Quality
-
-
- sixlabors.imagesharp.web.png?width=300&format=jpg&quality=100
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=jpg&quality=50
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=jpg&quality=1
-
-
-
-
-
-
-
-
- Webp Quality
-
-
- sixlabors.imagesharp.web.png?width=300&format=webp&quality=100
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=webp&quality=50
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&format=webp&quality=1
-
-
-
-
-
-
-
-
- Background Color
-
-
- sixlabors.imagesharp.web.png?width=300&bgcolor=red
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&bgcolor=FFFF00
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&bgcolor=128,28,32
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&bgcolor=C1FF0080
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=300&bgcolor=128,28,32,128
-
-
-
-
-
-
-
-
- Identical Queries
- Demonstrates that the middleware handles identical queries without file contention.
-
-
- sixlabors.imagesharp.web.png?width=123
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=123
-
-
-
-
-
-
-
- sixlabors.imagesharp.web.png?width=123
-
-
-
-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&ranchor=bottomleft
-
-

-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&ranchor=left&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&ranchor=right&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
-
- EXIF Handling Portrait
- Demonstrates that the middleware handles EXIF orientation correctly for portrait images.
-
-
- Portrait_0.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_1.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_2.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_3.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_4.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_5.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Portrait_6.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_7.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Portrait_8.jpg?width=60&height=50&ranchor=bottomright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&rxy=0.69,0.37
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&ranchor=topright
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&ranchor=top&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&ranchor=topright&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&ranchor=bottom&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- EXIF Handling : Landscape
- Demonstrates that the middleware handles EXIF orientation correctly for landscape images.
-
-
- Landscape_0.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_1.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_2.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_3.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_4.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_5.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
- Landscape_6.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_7.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
- Landscape_8.jpg?width=60&height=50&ranchor=bottomleft&rmode=pad&rcolor=limegreen
-
-

-
-
-
-
-
diff --git a/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg
new file mode 100644
index 00000000..b1ff27a5
--- /dev/null
+++ b/samples/ImageSharp.Web.Sample/wwwroot/sixlabors.imagesharp.web.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ImageSharp.Web/CaseHandlingUriBuilder.cs b/src/ImageSharp.Web/CaseHandlingUriBuilder.cs
index e3418820..6c5b4343 100644
--- a/src/ImageSharp.Web/CaseHandlingUriBuilder.cs
+++ b/src/ImageSharp.Web/CaseHandlingUriBuilder.cs
@@ -156,14 +156,12 @@ public static string Encode(CaseHandling handling, Uri uri)
pathBase: PathString.FromUriComponent(uri),
query: QueryString.FromUriComponent(uri));
}
- else
- {
- Uri faux = new(FallbackBaseUri, uri);
- return BuildRelative(
- handling,
- path: PathString.FromUriComponent(faux),
- query: QueryString.FromUriComponent(faux));
- }
+
+ Uri faux = new(FallbackBaseUri, uri);
+ return BuildRelative(
+ handling,
+ path: PathString.FromUriComponent(faux),
+ query: QueryString.FromUriComponent(faux));
}
///
@@ -202,12 +200,12 @@ private static void InitializeAbsoluteUriString(Span buffer, (bool Lower,
int index = 0;
ReadOnlySpan pathBaseSpan = uriParts.PathBase.AsSpan();
- if (uriParts.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[pathBaseSpan.Length - 1] == '/')
+ if (uriParts.Path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/')
{
// If the path string has a trailing slash and the other string has a leading slash, we need
// to trim one of them.
// Trim the last slash from pathBase. The total length was decremented before the call to string.Create.
- pathBaseSpan = pathBaseSpan.Slice(0, pathBaseSpan.Length - 1);
+ pathBaseSpan = pathBaseSpan[..^1];
}
if (uriParts.Scheme.Length > 0)
diff --git a/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs b/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs
index 92729e1f..4a3a3e59 100644
--- a/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs
@@ -57,7 +57,7 @@ private static void AddDefaultServices(
builder.SetRequestParser();
- builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.SetCache();
diff --git a/src/ImageSharp.Web/Format.cs b/src/ImageSharp.Web/Format.cs
new file mode 100644
index 00000000..6d91a67a
--- /dev/null
+++ b/src/ImageSharp.Web/Format.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Contains reusable static instances of known image formats.
+///
+public static class Format
+{
+ ///
+ /// Gets the Bmp encoding format.
+ ///
+ public static FormatCommand Bmp { get; } = new("bmp");
+
+ ///
+ /// Gets the Gif encoding format.
+ ///
+ public static FormatCommand Gif { get; } = new("gif");
+
+ ///
+ /// Gets the Jpg encoding format.
+ ///
+ public static FormatCommand Jpg { get; } = new("jpg");
+
+ ///
+ /// Gets the Png encoding format.
+ ///
+ public static FormatCommand Png { get; } = new("png");
+
+ ///
+ /// Gets the Tga encoding format.
+ ///
+ public static FormatCommand Tga { get; } = new("tga");
+
+ ///
+ /// Gets the WebP encoding format.
+ ///
+ public static FormatCommand WebP { get; } = new("webp");
+}
diff --git a/src/ImageSharp.Web/FormatCommand.cs b/src/ImageSharp.Web/FormatCommand.cs
new file mode 100644
index 00000000..536564c3
--- /dev/null
+++ b/src/ImageSharp.Web/FormatCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Represents a command for setting the type of format with which to encode processed images.
+///
+public readonly struct FormatCommand
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The name of the resampler command.
+ public FormatCommand(string name) => this.Name = name;
+
+ ///
+ /// Gets the name of the resampler command.
+ ///
+ public string Name { get; }
+}
diff --git a/src/ImageSharp.Web/HMACUtilities.cs b/src/ImageSharp.Web/HMACUtilities.cs
index f90d8c52..ee128aff 100644
--- a/src/ImageSharp.Web/HMACUtilities.cs
+++ b/src/ImageSharp.Web/HMACUtilities.cs
@@ -30,8 +30,7 @@ public static class HMACUtilities
/// The hashed .
public static unsafe string ComputeHMACSHA256(string value, byte[] secret)
{
- // TODO: In .NET 6 we can use single instance versions
- using var hmac = new HMACSHA256(secret);
+ using HMACSHA256 hmac = new(secret);
return CreateHMAC(value, hmac);
}
@@ -46,7 +45,7 @@ public static unsafe string ComputeHMACSHA256(string value, byte[] secret)
/// The hashed .
public static unsafe string ComputeHMACSHA384(string value, byte[] secret)
{
- using var hmac = new HMACSHA384(secret);
+ using HMACSHA384 hmac = new(secret);
return CreateHMAC(value, hmac);
}
@@ -61,7 +60,7 @@ public static unsafe string ComputeHMACSHA384(string value, byte[] secret)
/// The hashed .
public static unsafe string ComputeHMACSHA512(string value, byte[] secret)
{
- using var hmac = new HMACSHA512(secret);
+ using HMACSHA512 hmac = new(secret);
return CreateHMAC(value, hmac);
}
diff --git a/src/ImageSharp.Web/ImageSharp.Web.csproj b/src/ImageSharp.Web/ImageSharp.Web.csproj
index a4f69af8..f76f6f32 100644
--- a/src/ImageSharp.Web/ImageSharp.Web.csproj
+++ b/src/ImageSharp.Web/ImageSharp.Web.csproj
@@ -40,6 +40,7 @@
+
diff --git a/src/ImageSharp.Web/Middleware/ImageContext.cs b/src/ImageSharp.Web/Middleware/ImageContext.cs
index 697dc5e7..84e63ec9 100644
--- a/src/ImageSharp.Web/Middleware/ImageContext.cs
+++ b/src/ImageSharp.Web/Middleware/ImageContext.cs
@@ -182,6 +182,7 @@ private async Task ApplyResponseHeadersAsync(
TimeSpan maxAge)
{
this.response.StatusCode = statusCode;
+
if (statusCode < 400)
{
// These headers are returned for 200 and 304
diff --git a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
index 9b9e9b2b..b9837291 100644
--- a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
+++ b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
@@ -38,7 +38,7 @@ private static readonly ConcurrentTLruCache SourceMetadat
///
/// Used to temporarily store cached HMAC-s to reduce the overhead of HMAC token generation.
///
- private static readonly ConcurrentTLruCache HMACTokenLru
+ private static readonly ConcurrentTLruCache HMACTokenLru
= new(1024, TimeSpan.FromSeconds(30));
///
@@ -109,7 +109,7 @@ private static readonly ConcurrentTLruCache HMACTokenLru
///
/// Contains helpers that allow authorization of image requests.
///
- private readonly ImageSharpRequestAuthorizationUtilities authorizationUtilities;
+ private readonly RequestAuthorizationUtilities authorizationUtilities;
///
/// Initializes a new instance of the class.
@@ -140,7 +140,7 @@ public ImageSharpMiddleware(
CommandParser commandParser,
FormatUtilities formatUtilities,
AsyncKeyReaderWriterLock asyncKeyLock,
- ImageSharpRequestAuthorizationUtilities requestAuthorizationUtilities)
+ RequestAuthorizationUtilities requestAuthorizationUtilities)
{
Guard.NotNull(next, nameof(next));
Guard.NotNull(options, nameof(options));
@@ -211,7 +211,7 @@ private async Task Invoke(HttpContext httpContext, bool retry)
if (secret?.Length > 0)
{
checkHMAC = true;
- token = commands.GetValueOrDefault(ImageSharpRequestAuthorizationUtilities.TokenCommand);
+ token = commands.GetValueOrDefault(RequestAuthorizationUtilities.TokenCommand);
}
this.authorizationUtilities.StripUnknownCommands(commands);
@@ -244,13 +244,10 @@ private async Task Invoke(HttpContext httpContext, bool retry)
// At this point we know that this is an image request designed for processing via this middleware.
// Check for a token if required and reject if invalid.
- if (checkHMAC)
+ if (checkHMAC && (hmac != token || (hmac is null && commands.Count > 0)))
{
- if (token == null || hmac != token)
- {
- SetBadRequest(httpContext);
- return;
- }
+ SetBadRequest(httpContext);
+ return;
}
IImageResolver? sourceImageResolver = await provider.GetAsync(httpContext);
diff --git a/src/ImageSharp.Web/ImageSharpRequestAuthorizationUtilities.cs b/src/ImageSharp.Web/RequestAuthorizationUtilities.cs
similarity index 95%
rename from src/ImageSharp.Web/ImageSharpRequestAuthorizationUtilities.cs
rename to src/ImageSharp.Web/RequestAuthorizationUtilities.cs
index 7c1aede5..6c4849bc 100644
--- a/src/ImageSharp.Web/ImageSharpRequestAuthorizationUtilities.cs
+++ b/src/ImageSharp.Web/RequestAuthorizationUtilities.cs
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Web;
///
/// Contains various helper methods for authorizing image requests.
///
-public sealed class ImageSharpRequestAuthorizationUtilities
+public sealed class RequestAuthorizationUtilities
{
///
/// The command used by image requests for transporting Hash-based Message Authentication Code (HMAC) tokens.
@@ -29,14 +29,14 @@ public sealed class ImageSharpRequestAuthorizationUtilities
private readonly IServiceProvider serviceProvider;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The middleware configuration options.
/// An instance used to parse image requests for commands.
/// A collection of instances used to process images.
/// The command parser.
/// The service provider.
- public ImageSharpRequestAuthorizationUtilities(
+ public RequestAuthorizationUtilities(
IOptions options,
IRequestParser requestParser,
IEnumerable processors,
@@ -78,7 +78,7 @@ public void StripUnknownCommands(CommandCollection commands)
if (commands?.Count > 0)
{
// Strip out any unknown commands, if needed.
- var keys = new List(commands.Keys);
+ List keys = new(commands.Keys);
for (int i = keys.Count - 1; i >= 0; i--)
{
if (!this.knownCommands.Contains(keys[i]))
@@ -216,6 +216,11 @@ public void StripUnknownCommands(CommandCollection commands)
this.StripUnknownCommands(commands);
}
+ if (commands.Count == 0)
+ {
+ return null;
+ }
+
ImageCommandContext imageCommandContext = new(context, commands, this.commandParser, this.parserCulture);
return await this.options.OnComputeHMACAsync(imageCommandContext, secret);
}
@@ -229,8 +234,15 @@ public void StripUnknownCommands(CommandCollection commands)
///
/// Contains information about the current image request and parsed commands.
/// The computed HMAC.
- internal Task ComputeHMACAsync(ImageCommandContext context)
- => this.options.OnComputeHMACAsync(context, this.options.HMACSecretKey);
+ internal async Task ComputeHMACAsync(ImageCommandContext context)
+ {
+ if (context.Commands.Count == 0)
+ {
+ return null;
+ }
+
+ return await this.options.OnComputeHMACAsync(context, this.options.HMACSecretKey);
+ }
private static void ToComponents(
Uri uri,
diff --git a/src/ImageSharp.Web/Resampler.cs b/src/ImageSharp.Web/Resampler.cs
new file mode 100644
index 00000000..febeecc7
--- /dev/null
+++ b/src/ImageSharp.Web/Resampler.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Contains reusable static instances of known resampling algorithms.
+///
+public static class Resampler
+{
+ ///
+ public static ResamplerCommand Bicubic { get; } = new("bicubic");
+
+ ///
+ public static ResamplerCommand Box { get; } = new("box");
+
+ ///
+ public static ResamplerCommand CatmullRom { get; } = new("catmullrom");
+
+ ///
+ public static ResamplerCommand Hermite { get; } = new("hermite");
+
+ ///
+ public static ResamplerCommand Lanczos2 { get; } = new("lanczos2");
+
+ ///
+ public static ResamplerCommand Lanczos3 { get; } = new("lanczos3");
+
+ ///
+ public static ResamplerCommand Lanczos5 { get; } = new("lanczos5");
+
+ ///
+ public static ResamplerCommand Lanczos8 { get; } = new("lanczos8");
+
+ ///
+ public static ResamplerCommand MitchellNetravali { get; } = new("mitchellnetravali");
+
+ ///
+ public static ResamplerCommand NearestNeighbor { get; } = new("nearestneighbor");
+
+ ///
+ public static ResamplerCommand Robidoux { get; } = new("robidoux");
+
+ ///
+ public static ResamplerCommand RobidouxSharp { get; } = new("robidouxsharp");
+
+ ///
+ public static ResamplerCommand Spline { get; } = new("spline");
+
+ ///
+ public static ResamplerCommand Triangle { get; } = new("triangle");
+
+ ///
+ public static ResamplerCommand Welch { get; } = new("welch");
+}
diff --git a/src/ImageSharp.Web/ResamplerCommand.cs b/src/ImageSharp.Web/ResamplerCommand.cs
new file mode 100644
index 00000000..432ed8ec
--- /dev/null
+++ b/src/ImageSharp.Web/ResamplerCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Web;
+
+///
+/// Represents a command for setting the type of sampler to use for resampling operations.
+///
+public readonly struct ResamplerCommand
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The name of the resampler command.
+ public ResamplerCommand(string name) => this.Name = name;
+
+ ///
+ /// Gets the name of the resampler command.
+ ///
+ public string Name { get; }
+}
diff --git a/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs b/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs
new file mode 100644
index 00000000..3aef7500
--- /dev/null
+++ b/src/ImageSharp.Web/TagHelpers/HmacTokenTagHelper.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Text;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Mvc.TagHelpers;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Middleware;
+
+namespace SixLabors.ImageSharp.Web.TagHelpers;
+
+///
+/// A implementation targeting <img> element that allows the automatic generation of HMAC image processing protection tokens.
+///
+[HtmlTargetElement("img", Attributes = SrcAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+public class HmacTokenTagHelper : UrlResolutionTagHelper
+{
+ private const string SrcAttributeName = "src";
+
+ private readonly ImageSharpMiddlewareOptions options;
+ private readonly RequestAuthorizationUtilities authorizationUtilities;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The middleware configuration options.
+ /// Contains helpers that allow authorization of image requests.
+ /// The URL helper factory.
+ /// The HTML encorder.
+ public HmacTokenTagHelper(
+ IOptions options,
+ RequestAuthorizationUtilities authorizationUtilities,
+ IUrlHelperFactory urlHelperFactory,
+ HtmlEncoder htmlEncoder)
+ : base(urlHelperFactory, htmlEncoder)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(authorizationUtilities, nameof(authorizationUtilities));
+
+ this.options = options.Value;
+ this.authorizationUtilities = authorizationUtilities;
+ }
+
+ ///
+ public override int Order => 2;
+
+ ///
+ /// Gets or sets the source of the image.
+ ///
+ ///
+ /// Passed through to the generated HTML in all cases.
+ ///
+ [HtmlAttributeName(SrcAttributeName)]
+ public string? Src { get; set; }
+
+ ///
+ public override void Process(TagHelperContext context, TagHelperOutput output)
+ {
+ Guard.NotNull(context, nameof(context));
+ Guard.NotNull(output, nameof(output));
+
+ output.CopyHtmlAttribute(SrcAttributeName, context);
+ this.ProcessUrlAttribute(SrcAttributeName, output);
+
+ byte[] secret = this.options.HMACSecretKey;
+ if (secret is null || secret.Length == 0)
+ {
+ return;
+ }
+
+ // Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
+ // pipeline have touched the value. If the value is already encoded this ImageTagHelper may
+ // not function properly.
+ string? src = output.Attributes[SrcAttributeName]?.Value as string;
+ if (string.IsNullOrWhiteSpace(src))
+ {
+ return;
+ }
+
+ string? hmac = this.authorizationUtilities.ComputeHMAC(src, CommandHandling.Sanitize);
+ if (hmac is not null)
+ {
+ this.Src = AddQueryString(src, hmac);
+ output.Attributes.SetAttribute(SrcAttributeName, this.Src);
+ }
+ }
+
+ private static string AddQueryString(
+ ReadOnlySpan uri,
+ string hmac)
+ {
+ ReadOnlySpan uriToBeAppended = uri;
+ ReadOnlySpan anchorText = default;
+
+ // If there is an anchor, then the query string must be inserted before its first occurrence.
+ int anchorIndex = uri.IndexOf('#');
+ if (anchorIndex != -1)
+ {
+ anchorText = uri[anchorIndex..];
+ uriToBeAppended = uri[..anchorIndex];
+ }
+
+ int queryIndex = uriToBeAppended.IndexOf('?');
+ bool hasQuery = queryIndex != -1;
+
+ StringBuilder sb = new();
+
+ sb.Append(uriToBeAppended)
+ .Append(hasQuery ? '&' : '?')
+ .Append(UrlEncoder.Default.Encode(RequestAuthorizationUtilities.TokenCommand))
+ .Append('=')
+ .Append(UrlEncoder.Default.Encode(hmac))
+ .Append(anchorText);
+
+ return sb.ToString();
+ }
+}
diff --git a/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs b/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs
new file mode 100644
index 00000000..49fe91dd
--- /dev/null
+++ b/src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs
@@ -0,0 +1,367 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Globalization;
+using System.Text;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Mvc.TagHelpers;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Commands;
+using SixLabors.ImageSharp.Web.Middleware;
+using SixLabors.ImageSharp.Web.Processors;
+
+namespace SixLabors.ImageSharp.Web.TagHelpers;
+
+///
+/// A implementation targeting <img> element that allows the automatic generation image processing commands.
+///
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + WidthAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + HeightAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + AnchorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + ModeAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + XyAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + ColorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + CompandAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + OrientAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + AutoOrientAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + FormatAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + BgColorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + QualityAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+public class ImageTagHelper : UrlResolutionTagHelper
+{
+ private const string SrcAttributeName = "src";
+ private const string AttributePrefix = "imagesharp-";
+ private const string WidthAttributeName = AttributePrefix + ResizeWebProcessor.Width;
+ private const string HeightAttributeName = AttributePrefix + ResizeWebProcessor.Height;
+ private const string AnchorAttributeName = AttributePrefix + ResizeWebProcessor.Anchor;
+ private const string ModeAttributeName = AttributePrefix + ResizeWebProcessor.Mode;
+ private const string XyAttributeName = AttributePrefix + ResizeWebProcessor.Xy;
+ private const string ColorAttributeName = AttributePrefix + ResizeWebProcessor.Color;
+ private const string CompandAttributeName = AttributePrefix + ResizeWebProcessor.Compand;
+ private const string OrientAttributeName = AttributePrefix + ResizeWebProcessor.Orient;
+ private const string SamplerAttributeName = AttributePrefix + ResizeWebProcessor.Sampler;
+ private const string AutoOrientAttributeName = AttributePrefix + AutoOrientWebProcessor.AutoOrient;
+ private const string FormatAttributeName = AttributePrefix + FormatWebProcessor.Format;
+ private const string BgColorAttributeName = AttributePrefix + BackgroundColorWebProcessor.Color;
+ private const string QualityAttributeName = AttributePrefix + QualityWebProcessor.Quality;
+
+ private readonly ImageSharpMiddlewareOptions options;
+ private readonly CultureInfo parserCulture;
+ private readonly char separator;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The middleware configuration options.
+ /// Contains helpers that allow authorization of image requests.
+ /// The URL helper factory.
+ /// The HTML encorder.
+ public ImageTagHelper(
+ IOptions options,
+ RequestAuthorizationUtilities authorizationUtilities,
+ IUrlHelperFactory urlHelperFactory,
+ HtmlEncoder htmlEncoder)
+ : base(urlHelperFactory, htmlEncoder)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(authorizationUtilities, nameof(authorizationUtilities));
+
+ this.options = options.Value;
+ this.parserCulture = this.options.UseInvariantParsingCulture
+ ? CultureInfo.InvariantCulture
+ : CultureInfo.CurrentCulture;
+ this.separator = this.parserCulture.TextInfo.ListSeparator[0];
+ }
+
+ ///
+ public override int Order => 1;
+
+ ///
+ /// Gets or sets the src.
+ ///
+ ///
+ /// Passed through to the generated HTML in all cases.
+ ///
+ [HtmlAttributeName(SrcAttributeName)]
+ public string? Src { get; set; }
+
+ ///
+ /// Gets or sets the width in pixel units.
+ ///
+ ///
+ /// Passed through to the generated HTML in all cases.
+ ///
+ [HtmlAttributeName(WidthAttributeName)]
+ public int? Width { get; set; }
+
+ ///
+ /// Gets or sets the height in pixel units.
+ ///
+ ///
+ /// Passed through to the generated HTML in all cases.
+ ///
+ [HtmlAttributeName(HeightAttributeName)]
+ public int? Height { get; set; }
+
+ ///
+ /// Gets or sets the resize mode.
+ ///
+ [HtmlAttributeName(ModeAttributeName)]
+ public ResizeMode? ResizeMode { get; set; }
+
+ ///
+ /// Gets or sets the anchor position.
+ ///
+ [HtmlAttributeName(AnchorAttributeName)]
+ public AnchorPositionMode? AnchorPosition { get; set; }
+
+ ///
+ /// Gets or sets the center coordinates.
+ ///
+ [HtmlAttributeName(XyAttributeName)]
+ public PointF? Center { get; set; }
+
+ ///
+ /// Gets or sets the color to use as a background when padding an image.
+ ///
+ [HtmlAttributeName(ColorAttributeName)]
+ public Color? PadColor { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to compress
+ /// or expand individual pixel colors values on processing.
+ ///
+ [HtmlAttributeName(CompandAttributeName)]
+ public bool? Compand { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to factor embedded
+ /// EXIF orientation property values during processing.
+ ///
+ /// Defaults to .
+ [HtmlAttributeName(OrientAttributeName)]
+ public bool? Orient { get; set; }
+
+ ///
+ /// Gets or sets the sampling algorithm to use when resizing images.
+ ///
+ [HtmlAttributeName(SamplerAttributeName)]
+ public ResamplerCommand? Sampler { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to automatically
+ /// rotate/flip the iput image based on embedded EXIF orientation property values
+ /// before processing.
+ ///
+ [HtmlAttributeName(AutoOrientAttributeName)]
+ public bool? AutoOrient { get; set; }
+
+ ///
+ /// Gets or sets the image format to convert to.
+ ///
+ [HtmlAttributeName(FormatAttributeName)]
+ public FormatCommand? Format { get; set; }
+
+ ///
+ /// Gets or sets the background color of the image.
+ ///
+ [HtmlAttributeName(BgColorAttributeName)]
+ public Color? BackgroundColor { get; set; }
+
+ ///
+ /// Gets or sets the quality, that will be used to encode the image. Quality
+ /// index must be between 0 and 100 (compression from max to min).
+ ///
+ [HtmlAttributeName(QualityAttributeName)]
+ public int? Quality { get; set; }
+
+ ///
+ public override void Process(TagHelperContext context, TagHelperOutput output)
+ {
+ Guard.NotNull(context, nameof(context));
+ Guard.NotNull(output, nameof(output));
+
+ string? src = output.Attributes[SrcAttributeName]?.Value as string ?? this.Src;
+ if (string.IsNullOrWhiteSpace(src)
+ || src.StartsWith("http", StringComparison.OrdinalIgnoreCase)
+ || src.StartsWith("ftp", StringComparison.OrdinalIgnoreCase)
+ || src.StartsWith("data", StringComparison.OrdinalIgnoreCase))
+ {
+ base.Process(context, output);
+ return;
+ }
+
+ output.CopyHtmlAttribute(SrcAttributeName, context);
+ this.ProcessUrlAttribute(SrcAttributeName, output);
+
+ CommandCollection commands = new();
+ this.AddProcessingCommands(context, output, commands, this.parserCulture);
+
+ if (commands.Count > 0)
+ {
+ // Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
+ // pipeline have touched the value. If the value is already encoded this helper may
+ // not function properly.
+ src = output.Attributes[SrcAttributeName]?.Value as string;
+ src = AddQueryString(src, commands);
+ output.Attributes.SetAttribute(SrcAttributeName, src);
+ this.Src = src;
+ }
+ }
+
+ ///
+ /// Allows the addition of processing commands by inheriting classes.
+ ///
+ /// Contains information associated with the current HTML tag.
+ /// A stateful HTML element used to generate an HTML tag.
+ /// The command collection.
+ /// The culture to use when generating and processing commands.
+ protected virtual void AddProcessingCommands(
+ TagHelperContext context,
+ TagHelperOutput output,
+ CommandCollection commands,
+ CultureInfo commandCulture)
+ {
+ this.AddResizeCommands(output, commands);
+ this.AddAutoOrientCommands(commands);
+ this.AddFormatCommands(commands);
+ this.AddBackgroundColorCommands(commands);
+ this.AddQualityCommands(commands);
+ }
+
+ private void AddResizeCommands(TagHelperOutput output, CommandCollection commands)
+ {
+ // If no explicit width/height has been set on the image, set the attributes to match the
+ // width/height from the process commands if present.
+ int? width = output.Attributes.ContainsName(ResizeWebProcessor.Width)
+ ? int.Parse(output.Attributes[ResizeWebProcessor.Width].Value.ToString()!, this.parserCulture)
+ : null;
+
+ if (this.Width.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Width, this.Width.Value.ToString(this.parserCulture));
+ output.Attributes.SetAttribute(ResizeWebProcessor.Width, width ?? this.Width);
+ }
+
+ int? height = output.Attributes.ContainsName(ResizeWebProcessor.Height)
+ ? int.Parse(output.Attributes[ResizeWebProcessor.Height].Value.ToString()!, this.parserCulture)
+ : null;
+
+ if (this.Height.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Height, this.Height.Value.ToString(this.parserCulture));
+ output.Attributes.SetAttribute(ResizeWebProcessor.Height, height ?? this.Height);
+ }
+
+ if (this.ResizeMode.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Mode, this.ResizeMode.Value.ToString());
+ }
+
+ if (this.AnchorPosition.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Anchor, this.AnchorPosition.Value.ToString());
+ }
+
+ if (this.Center.HasValue)
+ {
+ string xy = $"{this.Center.Value.X.ToString(this.parserCulture)}{this.separator}{this.Center.Value.Y.ToString(this.parserCulture)}";
+ commands.Add(ResizeWebProcessor.Xy, xy);
+ }
+
+ if (this.PadColor.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Color, this.PadColor.Value.ToHex());
+ }
+
+ if (this.Compand.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Compand, this.Compand.Value.ToString(this.parserCulture));
+ }
+
+ if (this.Orient.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Orient, this.Orient.Value.ToString(this.parserCulture));
+ }
+
+ if (this.Sampler.HasValue)
+ {
+ commands.Add(ResizeWebProcessor.Sampler, this.Sampler.Value.Name);
+ }
+ }
+
+ private void AddAutoOrientCommands(CommandCollection commands)
+ {
+ if (this.AutoOrient.HasValue)
+ {
+ commands.Add(AutoOrientWebProcessor.AutoOrient, this.AutoOrient.Value.ToString());
+ }
+ }
+
+ private void AddFormatCommands(CommandCollection commands)
+ {
+ if (this.Format.HasValue)
+ {
+ commands.Add(FormatWebProcessor.Format, this.Format.Value.Name);
+ }
+ }
+
+ private void AddBackgroundColorCommands(CommandCollection commands)
+ {
+ if (this.BackgroundColor.HasValue)
+ {
+ commands.Add(BackgroundColorWebProcessor.Color, this.BackgroundColor.Value.ToHex());
+ }
+ }
+
+ private void AddQualityCommands(CommandCollection commands)
+ {
+ if (this.Quality.HasValue)
+ {
+ commands.Add(QualityWebProcessor.Quality, this.Quality.Value.ToString(this.parserCulture));
+ }
+ }
+
+ private static string AddQueryString(
+ ReadOnlySpan uri,
+ CommandCollection commands)
+ {
+ ReadOnlySpan uriToBeAppended = uri;
+ ReadOnlySpan anchorText = default;
+
+ // If there is an anchor, then the query string must be inserted before its first occurrence.
+ int anchorIndex = uri.IndexOf('#');
+ if (anchorIndex != -1)
+ {
+ anchorText = uri[anchorIndex..];
+ uriToBeAppended = uri[..anchorIndex];
+ }
+
+ int queryIndex = uriToBeAppended.IndexOf('?');
+ bool hasQuery = queryIndex != -1;
+
+ StringBuilder sb = new();
+ sb.Append(uriToBeAppended);
+
+ foreach (KeyValuePair parameter in commands)
+ {
+ if (parameter.Value is null)
+ {
+ continue;
+ }
+
+ sb.Append(hasQuery ? '&' : '?')
+ .Append(UrlEncoder.Default.Encode(parameter.Key))
+ .Append('=')
+ .Append(UrlEncoder.Default.Encode(parameter.Value));
+
+ hasQuery = true;
+ }
+
+ sb.Append(anchorText);
+ return sb.ToString();
+ }
+}
diff --git a/tests/ImageSharp.Web.Tests/TagHelpers/HmacTokenTagHelperTests.cs b/tests/ImageSharp.Web.Tests/TagHelpers/HmacTokenTagHelperTests.cs
new file mode 100644
index 00000000..62f15d53
--- /dev/null
+++ b/tests/ImageSharp.Web.Tests/TagHelpers/HmacTokenTagHelperTests.cs
@@ -0,0 +1,259 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Extensions.WebEncoders.Testing;
+using SixLabors.ImageSharp.Web.DependencyInjection;
+using SixLabors.ImageSharp.Web.Middleware;
+using SixLabors.ImageSharp.Web.TagHelpers;
+
+namespace SixLabors.ImageSharp.Web.Tests.TagHelpers;
+
+public sealed class HmacTokenTagHelperTests : IDisposable
+{
+ public HmacTokenTagHelperTests()
+ {
+ ServiceCollection services = new();
+ services.AddSingleton();
+ services.AddImageSharp(options => options.HMACSecretKey = new byte[] { 1, 2, 3, 4, 5 });
+ this.Provider = services.BuildServiceProvider();
+ }
+
+ public ServiceProvider Provider { get; }
+
+ [Fact]
+ public void RendersHmacTokenTag_SrcIncludes_HMAC()
+ {
+ // Arrange
+ TagHelperContext context = MakeTagHelperContext(
+ attributes: new TagHelperAttributeList
+ {
+ { "src", "testimage.png?width=50" },
+ { "width", 50 }
+ });
+
+ TagHelperOutput output = MakeImageTagHelperOutput(
+ attributes: new TagHelperAttributeList
+ {
+ { "width", 50 }
+ });
+
+ TagHelperOutput expectedOutput = MakeImageTagHelperOutput(
+ attributes: new TagHelperAttributeList
+ {
+ { "src", "testimage.png?width=50&hmac=54edff059ad28d0f0ec2494de1dce0e6152e8d26e53e2efb249cdae93e30acbc" },
+ { "width", 50 }
+ });
+
+ HmacTokenTagHelper helper = this.GetHelper();
+ helper.Src = "testimage.png?width=50";
+
+ // Act
+ helper.Process(context, output);
+
+ // Assert
+ Assert.Equal(expectedOutput.TagName, output.TagName);
+ Assert.Equal(2, output.Attributes.Count);
+
+ for (int i = 0; i < expectedOutput.Attributes.Count; i++)
+ {
+ TagHelperAttribute expectedAttribute = expectedOutput.Attributes[i];
+ TagHelperAttribute actualAttribute = output.Attributes[i];
+ Assert.Equal(expectedAttribute.Name, actualAttribute.Name);
+ Assert.Equal(expectedAttribute.Value.ToString(), actualAttribute.Value.ToString(), ignoreCase: true);
+ }
+ }
+
+ private HmacTokenTagHelper GetHelper(
+ IUrlHelperFactory urlHelperFactory = null,
+ ViewContext viewContext = null)
+ {
+ urlHelperFactory ??= new FakeUrlHelperFactory();
+ viewContext ??= MakeViewContext();
+
+ return new HmacTokenTagHelper(
+ this.Provider.GetRequiredService>(),
+ this.Provider.GetRequiredService(),
+ urlHelperFactory,
+ new HtmlTestEncoder())
+ {
+ ViewContext = viewContext,
+ };
+ }
+
+ private static TagHelperContext MakeTagHelperContext(
+ TagHelperAttributeList attributes)
+ => new(
+ tagName: "image",
+ allAttributes: attributes,
+ items: new Dictionary