2026-02-15 01:03:26 +01:00
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2026-02-15 00:14:04 +01:00
|
|
|
using System.Threading.Tasks;
|
2026-02-15 11:13:23 +01:00
|
|
|
// System.Drawing not required for ImageSharp-based drawing in this class
|
2026-02-15 00:14:04 +01:00
|
|
|
using Microsoft.Extensions.Logging;
|
2026-02-15 01:03:26 +01:00
|
|
|
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;
|
2026-02-15 11:13:23 +01:00
|
|
|
using SixLabors.Fonts;
|
|
|
|
|
using SixLabors.ImageSharp.Drawing.Processing;
|
|
|
|
|
using SixLabors.ImageSharp.Drawing;
|
2026-02-15 00:14:04 +01:00
|
|
|
|
|
|
|
|
namespace MaddoShared;
|
|
|
|
|
|
2026-02-15 01:03:26 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// 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.
|
|
|
|
|
/// </summary>
|
2026-02-21 15:53:52 +01:00
|
|
|
public class ImageCreatorImageSharp : IImageCreator
|
2026-02-15 00:14:04 +01:00
|
|
|
{
|
2026-02-15 01:03:26 +01:00
|
|
|
private readonly PicSettings _picSettings;
|
2026-02-21 15:53:52 +01:00
|
|
|
private readonly ILogger<ImageCreatorImageSharp> _logger;
|
2026-02-15 00:14:04 +01:00
|
|
|
|
2026-02-21 15:53:52 +01:00
|
|
|
public ImageCreatorImageSharp(PicSettings picSettings, ILogger<ImageCreatorImageSharp> logger)
|
2026-02-15 00:14:04 +01:00
|
|
|
{
|
2026-02-15 01:03:26 +01:00
|
|
|
_picSettings = picSettings ?? throw new ArgumentNullException(nameof(picSettings));
|
2026-02-15 00:14:04 +01:00
|
|
|
_logger = logger;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:17:23 +01:00
|
|
|
public async Task CreateImageAsync(ImageState imgState, byte[]? logoData)
|
2026-02-15 01:03:26 +01:00
|
|
|
{
|
|
|
|
|
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<Rgba32>(fs).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
// Extract EXIF info (orientation and creation date)
|
|
|
|
|
ExtractExif(img, imgState);
|
|
|
|
|
|
|
|
|
|
// Apply orientation
|
2026-02-21 15:43:26 +01:00
|
|
|
// Set rotation flags and apply orientation so downstream logic can
|
|
|
|
|
// use imgState.FotoRuotaADestra / FotoRuotaASinistra to decide which
|
|
|
|
|
// text to draw (horizontal vs vertical).
|
2026-02-15 01:03:26 +01:00
|
|
|
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);
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// Draw overlays (text/logo) onto big image using ImageSharp and save
|
2026-02-26 19:17:23 +01:00
|
|
|
await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false);
|
2026-02-15 01:03:26 +01:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// Draw overlays and save thumbnail via ImageSharp
|
2026-02-26 19:17:23 +01:00
|
|
|
await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false);
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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 },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:17:23 +01:00
|
|
|
private async Task DrawAndSaveWithGdiAsync(Image<Rgba32> imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail)
|
2026-02-15 01:03:26 +01:00
|
|
|
{
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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();
|
|
|
|
|
|
2026-02-21 14:12:15 +01:00
|
|
|
// 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// Ensure DataFoto is set (extracted earlier) so time-based text is available
|
|
|
|
|
imgState.DataFoto = imgState.CreationDate ?? DateTime.Now;
|
2026-02-15 01:03:26 +01:00
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-15 01:03:26 +01:00
|
|
|
|
2026-02-21 15:43:26 +01:00
|
|
|
// 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);
|
2026-02-15 01:03:26 +01:00
|
|
|
if (string.IsNullOrEmpty(text) && _picSettings.TestoNome)
|
|
|
|
|
text = imgState.NomeFileBig;
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(text))
|
|
|
|
|
{
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-21 15:43:26 +01:00
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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;
|
|
|
|
|
|
2026-02-17 20:51:35 +01:00
|
|
|
// 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;
|
|
|
|
|
|
2026-02-21 15:43:26 +01:00
|
|
|
// 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);
|
2026-02-15 11:13:23 +01:00
|
|
|
|
|
|
|
|
// 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
|
2026-02-17 20:51:35 +01:00
|
|
|
var approxWidth = finalFont.Size * longestLine.Length * 0.6f;
|
|
|
|
|
// Account for multiple lines: height = font size * lineCount * lineSpacing
|
|
|
|
|
var approxHeight = finalFont.Size * lines.Length * lineSpacing;
|
2026-02-15 11:13:23 +01:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
2026-02-15 01:03:26 +01:00
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
originX = Math.Max(0, Math.Min(originX, working.Width - approxWidth));
|
|
|
|
|
originY = Math.Max(0, Math.Min(originY, working.Height - approxHeight));
|
2026-02-15 01:03:26 +01:00
|
|
|
|
2026-02-17 20:51:35 +01:00
|
|
|
// Draw shadow then text; handle multiple lines
|
|
|
|
|
var lineHeight = finalFont.Size * lineSpacing;
|
2026-02-15 11:13:23 +01:00
|
|
|
working.Mutate(ctx =>
|
|
|
|
|
{
|
2026-02-17 20:51:35 +01:00
|
|
|
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;
|
|
|
|
|
|
2026-02-21 15:46:03 +01:00
|
|
|
// 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));
|
2026-02-17 20:51:35 +01:00
|
|
|
}
|
2026-02-15 11:13:23 +01:00
|
|
|
});
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:51:35 +01:00
|
|
|
// 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).
|
2026-02-26 19:17:23 +01:00
|
|
|
if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail)
|
2026-02-15 01:03:26 +01:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-02-15 11:13:23 +01:00
|
|
|
Image<Rgba32> logoImg = null;
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(_picSettings.LogoNomeFile) && File.Exists(_picSettings.LogoNomeFile))
|
|
|
|
|
{
|
|
|
|
|
using var logoStream = File.OpenRead(_picSettings.LogoNomeFile);
|
|
|
|
|
logoImg = await SixLabors.ImageSharp.Image.LoadAsync<Rgba32>(logoStream).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-02-26 19:17:23 +01:00
|
|
|
using var ms = new MemoryStream(logoData);
|
2026-02-15 11:13:23 +01:00
|
|
|
logoImg = await SixLabors.ImageSharp.Image.LoadAsync<Rgba32>(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();
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-02-15 11:13:23 +01:00
|
|
|
_logger.LogError(ex, "[Alternate] Error drawing logo in ImageSharp pass");
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure directory
|
|
|
|
|
var dir = System.IO.Path.GetDirectoryName(outputPath);
|
|
|
|
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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);
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// Removed GDI encoder helper; ImageSharp encoders are used instead.
|
2026-02-15 01:03:26 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
// 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;
|
2026-02-21 15:43:26 +01:00
|
|
|
// Ensure vertical text is populated from settings so callers can
|
|
|
|
|
// select the proper variant when drawing vertical photos.
|
|
|
|
|
imgState.TestoFirmaV ??= _picSettings.TestoFirmaStartV ?? string.Empty;
|
2026-02-15 11:13:23 +01:00
|
|
|
imgState.TestoFirmaPiccola ??= string.Empty;
|
|
|
|
|
imgState.DataPartenzaI = _picSettings.DataPartenza;
|
|
|
|
|
imgState.TestoOrario = _picSettings.TestoOrario ?? string.Empty;
|
|
|
|
|
|
2026-02-15 01:03:26 +01:00
|
|
|
// 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<Rgba32> img, ImageState imgState)
|
|
|
|
|
{
|
|
|
|
|
imgState.Orientation = Orientations.TopLeft;
|
|
|
|
|
imgState.CreationDate = null;
|
|
|
|
|
|
|
|
|
|
var profile = img.Metadata?.ExifProfile;
|
|
|
|
|
if (profile is null) return;
|
|
|
|
|
|
|
|
|
|
IExifValue<ushort> rotation = null;
|
|
|
|
|
var found = profile.TryGetValue(ExifTag.Orientation, out rotation);
|
|
|
|
|
if (found && rotation != null)
|
|
|
|
|
{
|
|
|
|
|
imgState.Orientation = (Orientations)Convert.ToInt32(rotation.Value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IExifValue<string> 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<Rgba32> img, ImageState imgState)
|
2026-02-15 00:14:04 +01:00
|
|
|
{
|
2026-02-15 01:03:26 +01:00
|
|
|
// Common EXIF orientations: 1=TopLeft, 3=BottomRight (rotate 180), 6=RightTop (rotate 90 CW), 8=LeftBottom (rotate 270 CW)
|
2026-02-21 15:43:26 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 01:03:26 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-21 14:12:15 +01:00
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
}
|
2026-02-15 01:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-15 00:14:04 +01:00
|
|
|
}
|
2026-02-15 11:13:23 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-02-15 00:14:04 +01:00
|
|
|
}
|
2026-02-15 01:03:26 +01:00
|
|
|
|