using AngleSharp.Dom; using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; namespace Nuuru.Server.Services; public sealed class EmbeddedImageDimensionsHtmlEnricher : IHtmlEnricher { public HtmlEnrichmentPhase Phase => HtmlEnrichmentPhase.Immediate; private readonly ApplicationDbContext _context; private readonly int _thumbnailMaxWidth; private readonly int _thumbnailMaxHeight; public EmbeddedImageDimensionsHtmlEnricher( ApplicationDbContext context, IConfiguration configuration) { _context = context; _thumbnailMaxWidth = configuration.GetValue("Thumbnails:MaxWidth", 300); _thumbnailMaxHeight = configuration.GetValue("Thumbnails:MaxHeight", 300); } public async Task EnrichAsync(IDocument document, HtmlEnrichmentContext context, CancellationToken cancellationToken = default) { if (context.Target != HtmlEnrichmentTarget.ForumPost) return false; var images = document.QuerySelectorAll("img").OfType().ToList(); if (images.Count == 0) return false; var attachmentIds = images .Select(GetAttachmentId) .Where(id => id.HasValue) .Select(id => id!.Value) .Distinct() .ToList(); Dictionary attachmentDimensions; if (attachmentIds.Count == 0) { attachmentDimensions = []; } else { attachmentDimensions = await _context.ForumPostAttachments .AsNoTracking() .Where(attachment => attachmentIds.Contains(attachment.Id) && attachment.Width.HasValue && attachment.Height.HasValue) .ToDictionaryAsync( attachment => attachment.Id, attachment => (attachment.Width!.Value, attachment.Height!.Value), cancellationToken); } var changed = false; foreach (var image in images) { var attachmentId = GetAttachmentId(image); if (!attachmentId.HasValue) continue; if (!attachmentDimensions.TryGetValue(attachmentId.Value, out var dimensions)) continue; var src = image.GetAttribute("src"); var targetDimensions = src?.Contains("/thumb", StringComparison.OrdinalIgnoreCase) == true ? ScaleToFit(dimensions.Width, dimensions.Height, _thumbnailMaxWidth, _thumbnailMaxHeight) : dimensions; if (HasDimensions(image) && image.GetAttribute("width") == targetDimensions.Width.ToString() && image.GetAttribute("height") == targetDimensions.Height.ToString()) { continue; } image.SetAttribute("width", targetDimensions.Width.ToString()); image.SetAttribute("height", targetDimensions.Height.ToString()); changed = true; } return changed; } private static Guid? GetAttachmentId(IElement image) { var rawAttachmentId = image.GetAttribute("data-attachment-id"); return Guid.TryParse(rawAttachmentId, out var attachmentId) ? attachmentId : null; } private static bool HasDimensions(IElement image) { return int.TryParse(image.GetAttribute("width"), out var width) && width > 0 && int.TryParse(image.GetAttribute("height"), out var height) && height > 0; } private static (int Width, int Height) ScaleToFit(int width, int height, int maxWidth, int maxHeight) { if (width <= 0 || height <= 0 || maxWidth <= 0 || maxHeight <= 0) return (width, height); var scale = Math.Min(1d, Math.Min((double)maxWidth / width, (double)maxHeight / height)); return ((int)Math.Max(1, Math.Round(width * scale)), (int)Math.Max(1, Math.Round(height * scale))); } }