From 3adb7ecb1cabcc579b68350e51061299f86b20e9 Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Sun, 13 Oct 2024 17:20:18 +0200 Subject: [PATCH] Enhances image manipulation with format (webp encoder, defaults to png) - computes etag with all manipulation parameters --- Oqtane.Server/Controllers/FileController.cs | 6 +- Oqtane.Server/Pages/Files.cshtml.cs | 75 ++++++++++++----- Oqtane.Server/Services/ImageService.cs | 93 ++++++++++++++------- Oqtane.Shared/Interfaces/IImageService.cs | 4 +- 4 files changed, 124 insertions(+), 54 deletions(-) diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 883a69e2..1e8a1740 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -685,14 +685,16 @@ namespace Oqtane.Controllers { if (!bool.TryParse(recreate, out _)) recreate = "false"; - string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); + string format = "png"; + + string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate)) { // user has edit access to folder or folder supports the image size being created if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || (!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString())))) { - imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, imagepath); + imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, format, imagepath); } else { diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index f27776bc..463534fc 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -102,7 +102,16 @@ namespace Oqtane.Pages // appends the query string to the redirect url if (Request.QueryString.HasValue && !string.IsNullOrWhiteSpace(Request.QueryString.Value)) { - url += Request.QueryString.Value; + if (url.Contains('?')) + { + url += "&"; + } + else + { + url += "?"; + } + + url += Request.QueryString.Value.Substring(1); } return RedirectPermanent(url); @@ -122,25 +131,49 @@ namespace Oqtane.Pages string downloadName = file.Name; string filepath = _files.GetFilePath(file); - bool hasWidthParam = Request.Query.TryGetValue("width", out var widthStr); - bool hasHeightParam = Request.Query.TryGetValue("height", out var heightStr); + var etagValue = file.ModifiedOn.Ticks ^ file.Size; + + bool isRequestingImageManipulation = false; int width = 0; int height = 0; - - bool isRequestingImageResize = - hasWidthParam && int.TryParse(widthStr, out width) && width > 0 && - hasHeightParam && int.TryParse(heightStr, out height) && height > 0; - - if (isRequestingImageResize) + if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0) { - etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size ^ (width * 31) ^ (height * 17), 16); + isRequestingImageManipulation = true; + etagValue ^= (width * 31); } - else + if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0) { - etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16); + isRequestingImageManipulation = true; + etagValue ^= (height * 17); } + Request.Query.TryGetValue("mode", out var mode); + Request.Query.TryGetValue("position", out var position); + Request.Query.TryGetValue("background", out var background); + + if (width > 0 || height > 0) + { + if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode(); + if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode(); + if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode(); + } + + int rotate; + if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0) + { + isRequestingImageManipulation = true; + etagValue ^= (rotate * 13); + } + + if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString())) + { + isRequestingImageManipulation = true; + etagValue ^= format.ToString().GetHashCode(); + } + + etag = Convert.ToString(etagValue, 16); + var header = ""; if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) { @@ -160,7 +193,7 @@ namespace Oqtane.Pages return BrokenFile(); } - if (isRequestingImageResize) + if (isRequestingImageManipulation) { var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue; _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; @@ -172,22 +205,24 @@ namespace Oqtane.Pages return BrokenFile(); } - Request.Query.TryGetValue("mode", out var mode); - Request.Query.TryGetValue("position", out var position); - Request.Query.TryGetValue("background", out var background); - Request.Query.TryGetValue("rotate", out var rotate); Request.Query.TryGetValue("recreate", out var recreate); if (!bool.TryParse(recreate, out _)) recreate = "false"; + if (!_imageService.GetAvailableFormats().Contains(format.ToString())) format = "png"; + if (width == 0 && height == 0) + { + width = file.ImageWidth; + height = file.ImageHeight; + } - string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); + string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate)) { // user has edit access to folder or folder supports the image size being created if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || (!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString())))) { - imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, imagepath); + imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotateStr, format, imagepath); } else { @@ -204,7 +239,7 @@ namespace Oqtane.Pages return BrokenFile(); } - downloadName = file.Name.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); + downloadName = file.Name.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); filepath = imagepath; } diff --git a/Oqtane.Server/Services/ImageService.cs b/Oqtane.Server/Services/ImageService.cs index 34fbd613..e22f3b8e 100644 --- a/Oqtane.Server/Services/ImageService.cs +++ b/Oqtane.Server/Services/ImageService.cs @@ -5,21 +5,29 @@ using System.IO; using System; using SixLabors.ImageSharp; using Oqtane.Infrastructure; -using Oqtane.Interfaces; using Oqtane.Shared; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Webp; +using System.Linq; namespace Oqtane.Services { public class ImageService : IImageService { private readonly ILogManager _logger; + private static readonly string[] _formats = ["png", "webp"]; public ImageService(ILogManager logger) { _logger = logger; } - public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath) + public string[] GetAvailableFormats() + { + return _formats; + } + + public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string format, string imagepath) { try { @@ -29,6 +37,7 @@ namespace Oqtane.Services if (!Color.TryParseHex("#" + background, out _)) background = "transparent"; if (!int.TryParse(rotate, out _)) rotate = "0"; rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate; + if (!_formats.Contains(format)) format = "png"; using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read)) { @@ -39,43 +48,34 @@ namespace Oqtane.Services Enum.TryParse(mode, true, out ResizeMode resizemode); Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode); - PngEncoder encoder; + if (width == 0 && height == 0) + { + width = image.Width; + height = image.Height; + } + + IImageEncoder encoder; + var resizeOptions = new ResizeOptions + { + Mode = resizemode, + Position = anchorpositionmode, + Size = new Size(width, height) + }; if (background != "transparent") { - image.Mutate(x => x - .AutoOrient() // auto orient the image - .Rotate(angle) - .Resize(new ResizeOptions - { - Mode = resizemode, - Position = anchorpositionmode, - Size = new Size(width, height), - PadColor = Color.ParseHex("#" + background) - })); - - encoder = new PngEncoder(); + resizeOptions.PadColor = Color.ParseHex("#" + background); + encoder = GetEncoder(format, transparent: false); } else { - image.Mutate(x => x + encoder = GetEncoder(format, transparent: true); + } + + image.Mutate(x => x .AutoOrient() // auto orient the image .Rotate(angle) - .Resize(new ResizeOptions - { - Mode = resizemode, - Position = anchorpositionmode, - Size = new Size(width, height) - })); - - encoder = new PngEncoder - { - ColorType = PngColorType.RgbWithAlpha, - TransparentColorMode = PngTransparentColorMode.Preserve, - BitDepth = PngBitDepth.Bit8, - CompressionLevel = PngCompressionLevel.BestSpeed - }; - } + .Resize(resizeOptions)); image.Save(imagepath, encoder); } @@ -89,5 +89,36 @@ namespace Oqtane.Services return imagepath; } + + private static IImageEncoder GetEncoder(string format, bool transparent) + { + return format switch + { + "png" => GetPngEncoder(transparent), + "webp" => GetWebpEncoder(transparent), + _ => GetPngEncoder(transparent), + }; + } + + private static PngEncoder GetPngEncoder(bool transparent) + { + return new PngEncoder() + { + ColorType = transparent ? PngColorType.RgbWithAlpha : PngColorType.Rgb, + TransparentColorMode = transparent ? PngTransparentColorMode.Preserve : PngTransparentColorMode.Clear, + BitDepth = PngBitDepth.Bit8, + CompressionLevel = PngCompressionLevel.BestSpeed + }; + } + + private static WebpEncoder GetWebpEncoder(bool transparent) + { + return new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 60, + TransparentColorMode = transparent ? WebpTransparentColorMode.Preserve : WebpTransparentColorMode.Clear, + }; + } } } diff --git a/Oqtane.Shared/Interfaces/IImageService.cs b/Oqtane.Shared/Interfaces/IImageService.cs index e20b6b04..f872b72d 100644 --- a/Oqtane.Shared/Interfaces/IImageService.cs +++ b/Oqtane.Shared/Interfaces/IImageService.cs @@ -8,6 +8,8 @@ namespace Oqtane.Services { public interface IImageService { - public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath); + public string[] GetAvailableFormats(); + + public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string format, string imagepath); } }