using System; using System.IO; using System.Threading.Tasks; // System.Drawing not required for ImageSharp-based drawing in this class using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using System.Linq; using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing; namespace MaddoShared; /// /// Image creator implemented using SixLabors.ImageSharp for core image operations. /// This implementation focuses on loading, EXIF-orientation, resizing and saving. /// It intentionally implements a minimal subset of the original functionality to /// provide a safe and testable replacement. Additional features (text/logo drawing) /// can be added later using ImageSharp.Drawing.Common and SixLabors.Fonts. /// public class ImageCreatorImageSharp : IImageCreator { private readonly PicSettings _picSettings; private readonly ILogger _logger; public ImageCreatorImageSharp(PicSettings picSettings, ILogger logger) { _picSettings = picSettings ?? throw new ArgumentNullException(nameof(picSettings)); _logger = logger; } public async Task CreateImageAsync(ImageState imgState, byte[]? logoData) { ArgumentNullException.ThrowIfNull(imgState); // Minimal preparation of names and settings normally done by ImageCreatorSharp.PrepareVariables PrepareVariablesMinimal(imgState); try { _logger.LogInformation("[Alternate] Processing {File} -> {Dest}", imgState.WorkFile?.FullName, imgState.DestDir?.FullName); using var fs = File.OpenRead(imgState.WorkFile.FullName); // Load as Rgba32 for general operations using var img = await SixLabors.ImageSharp.Image.LoadAsync(fs).ConfigureAwait(false); // Extract EXIF info (orientation and creation date) ExtractExif(img, imgState); // Apply orientation // Set rotation flags and apply orientation so downstream logic can // use imgState.FotoRuotaADestra / FotoRuotaASinistra to decide which // text to draw (horizontal vs vertical). ApplyExifOrientation(img, imgState); // Determine output format var forceJpg = _picSettings.UsaForzaJpg; // Compute big size var bigSize = ComputeBigSize(img.Width, img.Height); // Resize big image if needed using var imgBig = img.Clone(ctx => ctx.Resize(bigSize.Width, bigSize.Height)); // Ensure destination exists imgState.DestDir?.Create(); var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig); // Draw overlays (text/logo) onto big image using ImageSharp and save await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false); // Create thumbnail if requested if (_picSettings.CreaMiniature) { var smallSize = ComputeSmallSize(img.Width, img.Height); using var imgSmall = img.Clone(ctx => ctx.Resize(smallSize.Width, smallSize.Height)); var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall); // Draw overlays and save thumbnail via ImageSharp await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false); } } catch (Exception ex) { _logger.LogError(ex, "[Alternate] Error processing image {File}", imgState.WorkFile?.Name); throw; } } // Thumbnail overlays are rendered by the GDI+ pass in DrawAndSaveWithGdiAsync to match original rendering. private static SixLabors.ImageSharp.Formats.IImageEncoder GetEncoderForExtension(string ext, long quality) { quality = Math.Clamp(quality, 1, 100); return ext switch { ".png" => new SixLabors.ImageSharp.Formats.Png.PngEncoder(), ".gif" => new SixLabors.ImageSharp.Formats.Gif.GifEncoder(), _ => new JpegEncoder { Quality = (int)quality }, }; } private async Task DrawAndSaveWithGdiAsync(Image imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail) { // Use ImageSharp drawing APIs to render text and logos and save using ImageSharp encoders. // Clone editable image so we don't mutate the original reference unexpectedly. using var working = imgSharp.Clone(); // Ensure the EXIF orientation tag is not present on the working image so // the final saved file has correct upright pixels and won't be // re-oriented by EXIF-aware viewers. try { working.Metadata?.ExifProfile?.RemoveValue(ExifTag.Orientation); } catch (Exception ex) { _logger?.LogDebug(ex, "[Alternate] Failed to clear EXIF orientation on working image"); } // Ensure DataFoto is set (extracted earlier) so time-based text is available imgState.DataFoto = imgState.CreationDate ?? DateTime.Now; // Ensure thumbnail text is prepared similarly to ImageCreatorSharp logic if (isThumbnail) { if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola)) { if (_picSettings.TestoMin) imgState.TestoFirmaPiccola = imgState.NomeFileBig; else if (_picSettings.AggNumTempMin) imgState.TestoFirmaPiccola = imgState.NomeFileBig + " "; else if (_picSettings.UsaOrarioMiniatura) imgState.TestoFirmaPiccola = imgState.DataFoto.ToShortTimeString(); else imgState.TestoFirmaPiccola ??= string.Empty; } } // Prepare text. For rotated (vertical) photos prefer the vertical text // variant coming from the viewmodel/settings (TestoFirmaStartV). var text = isThumbnail ? imgState.TestoFirmaPiccola : ((imgState.FotoRuotaADestra || imgState.FotoRuotaASinistra) ? imgState.TestoFirmaV : imgState.TestoFirma); if (string.IsNullOrEmpty(text) && _picSettings.TestoNome) text = imgState.NomeFileBig; if (!string.IsNullOrEmpty(text)) { // Choose target starting font size var baseSize = isThumbnail ? imgState.DimensioneStandardMiniatura : imgState.DimensioneStandard; // Create font with fallback SixLabors.Fonts.Font font; try { var fontName = _picSettings.IlFont ?? SixLabors.Fonts.SystemFonts.Collection.Families.First().Name; font = SixLabors.Fonts.SystemFonts.CreateFont(fontName, Math.Max(6, baseSize)); } catch { font = SixLabors.Fonts.SystemFonts.CreateFont(SixLabors.Fonts.SystemFonts.Collection.Families.First().Name, Math.Max(6, baseSize)); } // Color with alpha var textColor = SixLabors.ImageSharp.Color.Black; try { var c = _picSettings.FontColoreRGB; textColor = SixLabors.ImageSharp.Color.FromRgba(c.R, c.G, c.B, (byte)imgState.AlphaScelta); } catch { textColor = SixLabors.ImageSharp.Color.Black; } var shadowColor = SixLabors.ImageSharp.Color.FromRgba(0, 0, 0, (byte)imgState.AlphaScelta); // Find best font size so text fits image width (allow 95% width) and not exceed ~15% height var maxTextWidth = working.Width * 0.95f; var maxTextHeight = working.Height * 0.15f; // Support multi-line text: measure using the longest line when choosing font size var normalizedText = text.Replace("\r", string.Empty); var lines = normalizedText.Split(new[] { '\n' }, StringSplitOptions.None); var longestLine = lines.OrderByDescending(l => l.Length).FirstOrDefault() ?? string.Empty; // Use a line spacing factor when computing allowed font size so multi-line // text is sized to fit vertically. The existing FindBestFontSize helper // approximates single-line height; to account for multiple lines pass a // reduced maxHeight to that helper. var lineSpacing = 1.1f; // small extra spacing between lines var adjustedMaxHeight = Math.Max(6f, maxTextHeight / (lines.Length * lineSpacing)); var chosenSize = FindBestFontSize(longestLine, font.Name, Math.Max(6, baseSize), maxTextWidth, adjustedMaxHeight); // Use final font var finalFont = SixLabors.Fonts.SystemFonts.CreateFont(font.Name, chosenSize); if (!isThumbnail) imgState.DimensioneStandard = (int)Math.Round(chosenSize); // Approximate measured size since TextMeasurer/RendererOptions may not be available in this Fonts version var approxWidth = finalFont.Size * longestLine.Length * 0.6f; // Account for multiple lines: height = font size * lineCount * lineSpacing var approxHeight = finalFont.Size * lines.Length * lineSpacing; // Compute horizontal position based on alignment float xCenterOfImg; switch ((_picSettings.Allineamento ?? string.Empty).ToUpperInvariant()) { case "SINISTRA": xCenterOfImg = Math.Min(_picSettings.Margine + (approxWidth / 2f), working.Width / 2f); break; case "DESTRA": xCenterOfImg = Math.Max(working.Width - _picSettings.Margine - (approxWidth / 2f), working.Width / 2f); break; default: xCenterOfImg = working.Width / 2f; break; } // Compute vertical position similar to ImageCreatorSharp behaviour float originY; var pos = (_picSettings.Posizione ?? string.Empty).ToUpperInvariant(); if (pos == "ALTO") { originY = _picSettings.Margine; } else if (pos == "BASSO") { originY = (float)(working.Height - approxHeight - (working.Height * _picSettings.Margine / 100.0)); } else { originY = (working.Height - approxHeight) / 2f; } // Compute origin X so the text is centered around xCenterOfImg var originX = xCenterOfImg - approxWidth / 2f; // Clamp origin so text remains inside the image bounds if (approxWidth > working.Width) { approxWidth = working.Width * 0.95f; } if (approxHeight > working.Height) { approxHeight = working.Height * 0.9f; } originX = Math.Max(0, Math.Min(originX, working.Width - approxWidth)); originY = Math.Max(0, Math.Min(originY, working.Height - approxHeight)); // Draw shadow then text; handle multiple lines var lineHeight = finalFont.Size * lineSpacing; working.Mutate(ctx => { for (int i = 0; i < lines.Length; i++) { var line = lines[i]; var y = originY + i * lineHeight; // Ensure we're drawing inside the canvas vertically if (y + finalFont.Size < 0 || y > working.Height) continue; // Center each line individually so multi-line (vertical) text is justified to center var approxWidthLine = finalFont.Size * (line?.Length ?? 0) * 0.6f; if (approxWidthLine > working.Width) approxWidthLine = working.Width * 0.95f; var x = xCenterOfImg - (approxWidthLine / 2f); x = Math.Max(0, Math.Min(x, working.Width - approxWidthLine)); ctx.DrawText(line, finalFont, shadowColor, new SixLabors.ImageSharp.PointF(x + 1, y + 1)); ctx.DrawText(line, finalFont, textColor, new SixLabors.ImageSharp.PointF(x, y)); } }); } // Draw logo if provided. For compatibility with the original GDI implementation, // do not draw the logo on thumbnails (ImageCreatorSharp only draws logos on big images). if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail) { try { Image logoImg = null; if (!string.IsNullOrEmpty(_picSettings.LogoNomeFile) && File.Exists(_picSettings.LogoNomeFile)) { using var logoStream = File.OpenRead(_picSettings.LogoNomeFile); logoImg = await SixLabors.ImageSharp.Image.LoadAsync(logoStream).ConfigureAwait(false); } else { using var ms = new MemoryStream(logoData); logoImg = await SixLabors.ImageSharp.Image.LoadAsync(ms).ConfigureAwait(false); } var fotoLogoH = _picSettings.LogoAltezza; var fotoLogoW = _picSettings.LogoLarghezza; // When rendering on thumbnails, limit logo size relative to the thumbnail dimensions var effectiveMaxLogoWidth = fotoLogoW; var effectiveMaxLogoHeight = fotoLogoH; if (isThumbnail) { // Do not allow the logo to occupy more than ~30% of the thumbnail area effectiveMaxLogoWidth = Math.Max(1, Math.Min(fotoLogoW, (int)(working.Width * 0.30))); effectiveMaxLogoHeight = Math.Max(1, Math.Min(fotoLogoH, (int)(working.Height * 0.30))); } // Compute new logo size preserving aspect ratio, but avoid upscaling the logo beyond its native size var targetByWidth = CalculateThumbnailSize(logoImg.Width, logoImg.Height, effectiveMaxLogoWidth, "Larghezza"); var targetByHeight = CalculateThumbnailSize(logoImg.Width, logoImg.Height, effectiveMaxLogoHeight, "Altezza"); var nuovaSize = (targetByWidth.Width <= targetByHeight.Width) ? targetByWidth : targetByHeight; // Prevent upscaling: clamp to original logo size if (nuovaSize.Width > logoImg.Width) nuovaSize.Width = logoImg.Width; if (nuovaSize.Height > logoImg.Height) nuovaSize.Height = logoImg.Height; // Parse logo margin (may be percentage string like "10%") var logoMargineStr = _picSettings.LogoMargine ?? string.Empty; var inPercentualeL = logoMargineStr.Trim().EndsWith('%'); var margineL = 0; if (inPercentualeL) { var trimmed = logoMargineStr.Trim().TrimEnd('%'); if (!int.TryParse(trimmed, out margineL)) margineL = 0; } else { if (!int.TryParse(logoMargineStr, out margineL)) margineL = 0; } var margineUsato = inPercentualeL ? Convert.ToInt32(working.Height * margineL / (double)100) : margineL; int xPosOfWm = 0; int yPosOfWm = 0; var logoH = (_picSettings.LogoPosizioneH ?? "NESSUNA").ToUpperInvariant(); var logoV = (_picSettings.LogoPosizioneV ?? "NESSUNA").ToUpperInvariant(); switch (logoH) { case "SINISTRA": case "NESSUNA": xPosOfWm = margineUsato; break; case "CENTRO": xPosOfWm = Convert.ToInt32((working.Width - nuovaSize.Width) / (double)2); break; case "DESTRA": xPosOfWm = ((working.Width - nuovaSize.Width) - margineUsato); break; } switch (logoV) { case "ALTO": case "NESSUNA": yPosOfWm = margineUsato; break; case "CENTRO": yPosOfWm = Convert.ToInt32((working.Height - nuovaSize.Height) / (double)2); break; case "BASSO": yPosOfWm = ((working.Height - nuovaSize.Height) - margineUsato); break; } var transparency = 1.0f; if (int.TryParse(_picSettings.LogoTrasparenza, out var lt)) transparency = Math.Clamp(lt / 100f, 0f, 1f); // Resize logo to nuovaSize logoImg.Mutate(x => x.Resize(nuovaSize.Width, nuovaSize.Height)); // If configured to use color-key transparency for non-PNG logos, replace the key color with transparent if (_picSettings.UseTransparentColor && !string.IsNullOrEmpty(_picSettings.TransparentColor)) { try { var hex = _picSettings.TransparentColor.Trim(); // Allow either #RRGGBB or RRGGBB if (hex.StartsWith("#")) hex = hex.Substring(1); if (hex.Length == 6) { var r = Convert.ToByte(hex.Substring(0, 2), 16); var g = Convert.ToByte(hex.Substring(2, 2), 16); var b = Convert.ToByte(hex.Substring(4, 2), 16); var key = SixLabors.ImageSharp.Color.FromRgb(r, g, b); // Replace matching pixels (exact match) with transparent logoImg.ProcessPixelRows(accessor => { for (int y = 0; y < accessor.Height; y++) { var row = accessor.GetRowSpan(y); for (int x = 0; x < row.Length; x++) { var p = row[x]; if (p.R == r && p.G == g && p.B == b) { row[x] = new Rgba32(p.R, p.G, p.B, 0); } } } }); } } catch (Exception ex) { _logger.LogError(ex, "[Alternate] Invalid transparent color setting {Color}", _picSettings.TransparentColor); } } // Ensure logo position keeps it inside the canvas (avoid clipping) xPosOfWm = Math.Max(0, Math.Min(xPosOfWm, working.Width - nuovaSize.Width)); yPosOfWm = Math.Max(0, Math.Min(yPosOfWm, working.Height - nuovaSize.Height)); // Draw logo with opacity working.Mutate(ctx => ctx.DrawImage(logoImg, new SixLabors.ImageSharp.Point(xPosOfWm, yPosOfWm), (float)transparency)); logoImg.Dispose(); } catch (Exception ex) { _logger.LogError(ex, "[Alternate] Error drawing logo in ImageSharp pass"); } } // Ensure directory var dir = System.IO.Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); // Save with requested quality using ImageSharp encoder var ext = System.IO.Path.GetExtension(outputPath)?.ToLowerInvariant() ?? string.Empty; var encoder = GetEncoderForExtension(ext, quality); await using var outStream = System.IO.File.Open(outputPath, System.IO.FileMode.Create, System.IO.FileAccess.Write); await working.SaveAsync(outStream, encoder).ConfigureAwait(false); } // Removed GDI encoder helper; ImageSharp encoders are used instead. private void PrepareVariablesMinimal(ImageState imgState) { imgState.NomeFileBig = imgState.WorkFile.Name; imgState.NomeFileSmall = (_picSettings.Suffisso ?? string.Empty) + imgState.WorkFile.Name; imgState.DimensioneStandard = _picSettings.DimStandard; imgState.DimensioneStandardMiniatura = _picSettings.DimStandardMiniatura; // basic text / transparency defaults used by drawing routines // AlphaScelta mirrors ImageCreatorSharp behavior: compute from PicSettings.Trasparenza (0-100) imgState.AlphaScelta = Convert.ToInt32((255 * (100 - _picSettings.Trasparenza) / (double)100)); // Set minimal text fields so text drawing has fallback values imgState.TestoFirma ??= _picSettings.TestoFirmaStart ?? string.Empty; // Ensure vertical text is populated from settings so callers can // select the proper variant when drawing vertical photos. imgState.TestoFirmaV ??= _picSettings.TestoFirmaStartV ?? string.Empty; imgState.TestoFirmaPiccola ??= string.Empty; imgState.DataPartenzaI = _picSettings.DataPartenza; imgState.TestoOrario = _picSettings.TestoOrario ?? string.Empty; // sanitize imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig); imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall); } private static string SanitizeFileName(string fileName) { if (string.IsNullOrEmpty(fileName)) return fileName; var invalid = System.IO.Path.GetInvalidFileNameChars(); var sb = new System.Text.StringBuilder(fileName.Length); foreach (var ch in fileName) sb.Append(Array.IndexOf(invalid, ch) >= 0 ? '_' : ch); return sb.ToString(); } private void ExtractExif(Image img, ImageState imgState) { imgState.Orientation = Orientations.TopLeft; imgState.CreationDate = null; var profile = img.Metadata?.ExifProfile; if (profile is null) return; IExifValue rotation = null; var found = profile.TryGetValue(ExifTag.Orientation, out rotation); if (found && rotation != null) { imgState.Orientation = (Orientations)Convert.ToInt32(rotation.Value); } IExifValue date = null; var creationFound = profile.TryGetValue(ExifTag.DateTimeOriginal, out date); if (creationFound && date != null) { if (DateTime.TryParseExact(date.Value, "yyyy:MM:dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var cr)) { imgState.CreationDate = cr; } else { imgState.CreationDate = null; } } else { imgState.CreationDate = null; } } private void ApplyExifOrientation(Image img, ImageState imgState) { // Common EXIF orientations: 1=TopLeft, 3=BottomRight (rotate 180), 6=RightTop (rotate 90 CW), 8=LeftBottom (rotate 270 CW) // Set rotation flags on the state so other code can pick the correct // text variant (vertical vs horizontal). Mirror ImageCreatorSharp logic. imgState.FotoRuotaADestra = false; imgState.FotoRuotaASinistra = false; if (_picSettings.UsaRotazioneAutomatica) { switch (imgState.Orientation) { case Orientations.BottomLeft: case Orientations.BottomRight: case Orientations.LeftTop: case Orientations.LftBottom: imgState.FotoRuotaASinistra = true; break; case Orientations.RightBottom: case Orientations.RightTop: case Orientations.TopLeft: case Orientations.TopRight: // no-op break; } } switch (imgState.Orientation) { case Orientations.RightTop: // 6 img.Mutate(x => x.Rotate(RotateMode.Rotate90)); break; case Orientations.BottomRight: // 3 img.Mutate(x => x.Rotate(RotateMode.Rotate180)); break; case Orientations.LftBottom: // 8 img.Mutate(x => x.Rotate(RotateMode.Rotate270)); break; default: break; } // After applying the pixel rotation, remove the EXIF orientation tag so // viewers do not re-apply the rotation. Leaving the tag causes some // viewers to display the image in the wrong (original) orientation. try { img.Metadata?.ExifProfile?.RemoveValue(ExifTag.Orientation); } catch (Exception ex) { // Non-fatal: log and continue _logger?.LogDebug(ex, "[Alternate] Could not clear EXIF orientation tag"); } } private System.Drawing.Size ComputeBigSize(int width, int height) { // If original large size option requested, return original // otherwise compute based on width/height limits from settings return width > height ? CalculateThumbnailSize(width, height, _picSettings.LarghezzaBig, "Larghezza") : CalculateThumbnailSize(width, height, _picSettings.AltezzaBig, "Altezza"); } private System.Drawing.Size ComputeSmallSize(int width, int height) { return width > height ? CalculateThumbnailSize(width, height, _picSettings.LarghezzaSmall, "Larghezza") : CalculateThumbnailSize(width, height, _picSettings.AltezzaSmall, "Altezza"); } // Helper to access PicSettings values via instance _picSettings private static System.Drawing.Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize) { double tempMultiplier; if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase)) tempMultiplier = maxPixel / (double)currentwidth; else if (string.Equals(tipoSize, "Altezza", StringComparison.OrdinalIgnoreCase)) tempMultiplier = maxPixel / (double)currentheight; else if (currentheight > currentwidth) tempMultiplier = maxPixel / (double)currentheight; else tempMultiplier = maxPixel / (double)currentwidth; var newSize = new System.Drawing.Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier)); return newSize; } private static float FindBestFontSize(string text, string fontName, int maxSize, float maxWidth, float maxHeight, int minSize = 6) { if (maxSize <= minSize) return Math.Max(minSize, maxSize); int low = minSize; int high = Math.Max(minSize, maxSize); int best = minSize; while (low <= high) { int mid = (low + high) / 2; // Approximate measurement: width ~ size * chars * 0.6, height ~ size var approxWidth = mid * text.Length * 0.6f; var approxHeight = mid * 1.0f; if (approxWidth <= maxWidth && approxHeight <= maxHeight) { best = mid; low = mid + 1; // try larger } else { high = mid - 1; // too big } } return best; } }