using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using SixLabors.ImageSharp.Web.Commands; namespace zero.Media.ImageSharp; /// /// Parses commands from the request querystring. /// public sealed class PresetRequestParser : IRequestParser { MediaOptions _options; ILogger _logger; const char COLON = ':'; const string PREFIX = "/:"; const char SLASH = '/'; const string MIME_AVIF = "image/avif"; const string MIME_WEBP = "image/webp"; public PresetRequestParser(ILogger logger, IOptionsMonitor monitor) { _logger = logger; _options = monitor.CurrentValue; monitor.OnChange(opts => _options = opts); } /// public CommandCollection ParseRequestCommands(string preset, HttpContext context = null) { string focalPoint = null; int focalIndex = preset.IndexOf(COLON); if (focalIndex > -1) { focalPoint = preset.Substring(focalIndex + 1, preset.Length - focalIndex - 1); preset = preset.Substring(0, focalIndex); } if (preset == "{rx}") { return []; } if (!_options.ImageSharp.Presets.TryGetValue(preset, out string[] transformed)) { _logger.LogWarning("Could not load imaging preset {preset}", preset); return []; } // get default processing commands (which are added to each request) from options Dictionary defaults = _options.ImageSharp.DefaultCommands.Select(x => x.Split("=", 2)).ToDictionary(x => x[0], x => x[1]); // fall back to webp, as avif is not supported yet by ImageSharp FallbackFormat fallbackFormat = FindFallbackFormatInContext(context); string format = defaults.GetValueOrDefault("format", fallbackFormat != FallbackFormat.None ? "webp" : null); string quality = defaults.GetValueOrDefault("quality"); CommandCollection filters = []; foreach (KeyValuePair kv in defaults) { string key = kv.Key; if (key != "format" && key != "quality") { filters[key] = kv.Value; } } foreach (string[] kv in transformed.Select(x => x.Split("=", 2))) { string key = kv[0]; string value = kv[1]; // skip format/quality filters because they need to be appended at the end if (key == "format") { format = value; continue; } if (key == "quality") { quality = value; continue; } filters[key] = value; } // add positioning filters if (focalPoint != null) { filters["rxy"] = focalPoint; } else { filters["ranchor"] = "center"; } // add format/quality filters if (format.HasValue()) { filters["format"] = format; } if (quality.HasValue()) { filters["quality"] = quality; } return filters; } /// public FallbackFormat FindFallbackFormatInContext(HttpContext context) { string acceptKey = Microsoft.Net.Http.Headers.HeaderNames.Accept; if (context == null || context.Request == null || !context.Request.Headers.ContainsKey(acceptKey)) { return FallbackFormat.None; } string acceptHeader = context.Request.Headers[acceptKey].ToString(); if (acceptHeader.IsNullOrEmpty()) { return FallbackFormat.None; } if (acceptHeader.Contains(MIME_AVIF)) { return FallbackFormat.Avif; } if (acceptHeader.Contains(MIME_WEBP)) { return FallbackFormat.Webp; } return FallbackFormat.None; } /// public CommandCollection ParseRequestCommands(HttpContext context) { string path = context.Request.Path.Value; int startSlash = path.IndexOf(PREFIX); int lastSlash = path.LastIndexOf(SLASH); if (startSlash < 0) { return new(); } string preset = path.Substring(startSlash + PREFIX.Length, lastSlash - startSlash - PREFIX.Length); return ParseRequestCommands(preset, context); } public enum FallbackFormat { None = 0, Webp = 1, Avif = 2 } }