diff --git a/CannedNet/Services/Controllers/ImageController.cs b/CannedNet/Services/Controllers/ImageController.cs index 8f17a69..ae16848 100644 --- a/CannedNet/Services/Controllers/ImageController.cs +++ b/CannedNet/Services/Controllers/ImageController.cs @@ -1,16 +1,16 @@ using CannedNet.Services.Infrastructure; using Microsoft.AspNetCore.Mvc; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Formats.Png; namespace CannedNet.Services.Controllers; [ApiController, Route("img")] public class ImageController : ControllerBase { - private static readonly byte[] PlaceholderPng; + private static readonly byte[] PlaceholderJpeg; static ImageController() { @@ -18,98 +18,90 @@ public class ImageController : ControllerBase using Image image = new(64, 64); image.Mutate(x => x.BackgroundColor(Color.FromRgb(32, 32, 32))); - using MemoryStream ms = new(); - image.SaveAsPng(ms); - PlaceholderPng = ms.ToArray(); + image.SaveAsJpeg(ms, new JpegEncoder { Quality = 50 }); + PlaceholderJpeg = ms.ToArray(); } [HttpGet("{imageName}")] public async Task GetImage(string imageName) { - var context = HttpContext; - + HttpContext context = HttpContext; string imagesDir = Path.Combine("Images"); Directory.CreateDirectory(imagesDir); - string filePath = Path.Combine(imagesDir, imageName); byte[] imageBytes; - bool fromFile = false; + string contentType; if (System.IO.File.Exists(filePath)) { imageBytes = await System.IO.File.ReadAllBytesAsync(filePath); - fromFile = true; + string ext = Path.GetExtension(imageName).ToLowerInvariant(); + contentType = ext switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".bmp" => "image/bmp", + _ => "image/png" + }; } else { - // IMPORTANT: placeholder is ONLY fallback, not treated as real asset - imageBytes = PlaceholderPng; + imageBytes = PlaceholderJpeg; + contentType = "image/jpeg"; } string? cropSquare = context.Request.Query["cropSquare"].FirstOrDefault(); string? widthStr = context.Request.Query["width"].FirstOrDefault(); string? heightStr = context.Request.Query["height"].FirstOrDefault(); - bool hasTransform = - !string.IsNullOrEmpty(widthStr) || - !string.IsNullOrEmpty(heightStr) || - (!string.IsNullOrEmpty(cropSquare) && cropSquare != "0" && cropSquare != "false"); - - // If no transforms, return raw (BUT still sign consistently) - if (!hasTransform) + if (string.IsNullOrEmpty(widthStr) && string.IsNullOrEmpty(heightStr) && string.IsNullOrEmpty(cropSquare)) { - context.Response.Headers["content-signature"] = - "key-id=KEY:RSA:p1.rec.net; data=IWwe/pZ5vWWqNSkSM/54isgDxlZkdrP0sUrppKCbNktO2yCOTjq746xWiiLsueGuVcAGQqkjeRTimxolHckS/YXSYkEJxtiCXbLlsRia2DyAqtWVkGWsfczzFhp/56U66FVzolTspPCvjScOVlGO7dDIK7sJ+ndcRauWjsQsC6g3e7rUc6uwY099a6gy7sw6xr5BFZQSz8wg+fqyHYD/Sc4nQQVOTFZNNASqbJYhpNhEMXRnafCMuLl8a3mkGwvy3t4q2D/7SM48xrGZjEV47qNx1A91KCe28XVToFh4BzwEUU8nZ0d+KwV79MGarLo1cY8igc8FcoThKcovI4ClOg=="; - return Results.File(imageBytes, "image/png"); + SignImageResponse(context, ref imageBytes); + return Results.File(imageBytes, contentType); } using Image image = Image.Load(imageBytes); - // Crop square + int resizeWidth = 0; + int resizeHeight = 0; + if (!string.IsNullOrEmpty(cropSquare) && cropSquare != "0" && cropSquare != "false") { int size = Math.Min(image.Width, image.Height); int x = (image.Width - size) / 2; int y = (image.Height - size) / 2; - image.Mutate(img => img.Crop(new Rectangle(x, y, size, size))); } - int.TryParse(widthStr, out int width); - int.TryParse(heightStr, out int height); + if (int.TryParse(widthStr, out int w)) + resizeWidth = w; - if (width > 0 || height > 0) + if (int.TryParse(heightStr, out int h)) + resizeHeight = h; + + if (resizeWidth > 0 || resizeHeight > 0) { - image.Mutate(img => img.Resize(width, height)); + image.Mutate(x => x.Resize(resizeWidth, resizeHeight)); } - // ALWAYS output PNG for deterministic bytes (critical for signatures) using MemoryStream output = new(); - await image.SaveAsPngAsync(output); - + await image.SaveAsJpegAsync(output, new JpegEncoder { Quality = 85 }); imageBytes = output.ToArray(); - - // IMPORTANT: signature must match final bytes EXACTLY - context.Response.Headers["content-signature"] = - "key-id=KEY:RSA:p1.rec.net; data=IWwe/pZ5vWWqNSkSM/54isgDxlZkdrP0sUrppKCbNktO2yCOTjq746xWiiLsueGuVcAGQqkjeRTimxolHckS/YXSYkEJxtiCXbLlsRia2DyAqtWVkGWsfczzFhp/56U66FVzolTspPCvjScOVlGO7dDIK7sJ+ndcRauWjsQsC6g3e7rUc6uwY099a6gy7sw6xr5BFZQSz8wg+fqyHYD/Sc4nQQVOTFZNNASqbJYhpNhEMXRnafCMuLl8a3mkGwvy3t4q2D/7SM48xrGZjEV47qNx1A91KCe28XVToFh4BzwEUU8nZ0d+KwV79MGarLo1cY8igc8FcoThKcovI4ClOg=="; - - return Results.File(imageBytes, "image/png"); + SignImageResponse(context, ref imageBytes); + return Results.File(imageBytes, "image/jpeg"); } private static void SignImageResponse(HttpContext context, ref byte[] imageBytes) { - if (context.Request.Query["sig"] != "p1") - return; + if (context.Request.Query["sig"] != "p1") return; string? signature = Signatures.Sign(imageBytes); - if (signature != null) { - // Keep EXACT format expected by client - context.Response.Headers["content-signature"] = - "key-id=KEY:RSA:p1.rec.net; data=" + signature; + context.Response.Headers["Content-Signature"] = $"key-id=KEY:RSA:p1.rec.net; data={signature}"; } } }