using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Microsoft.Extensions.Caching.Memory; namespace Nuuru.Server.Services { public interface IWatermarkService { Task ApplyAsync(Stream imageStream, string mimeType, string? cacheKey = null); } public class WatermarkService : IWatermarkService { private readonly ILogger _logger; private readonly IMemoryCache _cache; private readonly string _watermarkPath; private static readonly HashSet SupportedTypes = new(StringComparer.OrdinalIgnoreCase) { "image/jpeg", "image/png", "image/webp", "image/bmp" }; public WatermarkService( ILogger logger, IMemoryCache cache, IConfiguration configuration) { _logger = logger; _cache = cache; _watermarkPath = Path.GetFullPath( configuration.GetValue("Watermark:Path") ?? "Assets/watermark.png"); } public async Task ApplyAsync(Stream imageStream, string mimeType, string? cacheKey = null) { if (!SupportedTypes.Contains(mimeType)) return imageStream; if (!File.Exists(_watermarkPath)) { _logger.LogWarning("Watermark file not found at {Path}", _watermarkPath); return imageStream; } if (cacheKey != null && _cache.TryGetValue($"wm:{cacheKey}", out var cached)) { return new MemoryStream(cached!, writable: false); } try { if (imageStream.CanSeek) imageStream.Position = 0; using var image = await Image.LoadAsync(imageStream); using var watermark = await Image.LoadAsync(_watermarkPath); if (image.Width < watermark.Width) return ResetAndReturn(imageStream); using var strip = new Image(image.Width, watermark.Height); if (image.Width > watermark.Width) { int padWidth = image.Width - watermark.Width; for (int x = 0; x < padWidth; x++) { for (int y = 0; y < watermark.Height; y++) { strip[x, y] = watermark[0, y]; } } strip.Mutate(ctx => ctx.DrawImage(watermark, new Point(padWidth, 0), 1f)); } else { strip.Mutate(ctx => ctx.DrawImage(watermark, new Point(0, 0), 1f)); } using var result = new Image(image.Width, image.Height + watermark.Height); result.Mutate(ctx => { ctx.DrawImage(image, new Point(0, 0), 1f); ctx.DrawImage(strip, new Point(0, image.Height), 1f); }); var output = new MemoryStream(); await SaveInFormat(result, output, mimeType); if (cacheKey != null) { _cache.Set($"wm:{cacheKey}", output.ToArray(), new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(1), Size = output.Length, }); } output.Position = 0; return output; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to apply watermark, returning original image"); return ResetAndReturn(imageStream); } } private static Stream ResetAndReturn(Stream stream) { if (stream.CanSeek) stream.Position = 0; return stream; } private static async Task SaveInFormat(Image image, Stream output, string mimeType) { switch (mimeType.ToLowerInvariant()) { case "image/png": await image.SaveAsPngAsync(output); break; case "image/webp": await image.SaveAsWebpAsync(output); break; case "image/bmp": await image.SaveAsBmpAsync(output); break; default: await image.SaveAsJpegAsync(output); break; } } } }