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 01:03:26 +01:00
|
|
|
using System.Drawing;
|
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;
|
|
|
|
|
using System.Drawing.Imaging;
|
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-15 00:14:04 +01:00
|
|
|
public class ImageCreatorAlternate : IImageCreator
|
|
|
|
|
{
|
2026-02-15 01:03:26 +01:00
|
|
|
private readonly PicSettings _picSettings;
|
2026-02-15 00:14:04 +01:00
|
|
|
private readonly ILogger<ImageCreatorAlternate> _logger;
|
|
|
|
|
|
2026-02-15 01:03:26 +01:00
|
|
|
public ImageCreatorAlternate(PicSettings picSettings, ILogger<ImageCreatorAlternate> 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-15 01:03:26 +01:00
|
|
|
public async Task CreateImageAsync(ImageState imgState, System.Drawing.Image logo)
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
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 via GDI+ and save
|
|
|
|
|
await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logo, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
// Save big image with quality if JPEG
|
|
|
|
|
var extBig = System.IO.Path.GetExtension(imgState.NomeFileBig)?.ToLowerInvariant() ?? string.Empty;
|
|
|
|
|
var encoderBig = GetEncoderForExtension(extBig, _picSettings.JpegQuality);
|
|
|
|
|
await using (var outStream = System.IO.File.Open(fileNameBig, System.IO.FileMode.Create, System.IO.FileAccess.Write))
|
|
|
|
|
{
|
|
|
|
|
await imgBig.SaveAsync(outStream, encoderBig, default).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 GDI+
|
|
|
|
|
await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logo, _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<Rgba32> imgSharp, string outputPath, ImageState imgState, System.Drawing.Image logo, long quality, bool isThumbnail)
|
|
|
|
|
{
|
|
|
|
|
// Convert ImageSharp image to System.Drawing.Bitmap via MemoryStream PNG to preserve alpha
|
|
|
|
|
await using var ms = new MemoryStream();
|
|
|
|
|
await imgSharp.SaveAsPngAsync(ms).ConfigureAwait(false);
|
|
|
|
|
ms.Seek(0, SeekOrigin.Begin);
|
|
|
|
|
|
|
|
|
|
using var bmp = new Bitmap(ms);
|
|
|
|
|
using var g = Graphics.FromImage(bmp);
|
|
|
|
|
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
|
|
|
|
|
|
|
|
// Prepare text
|
|
|
|
|
var text = isThumbnail ? imgState.TestoFirmaPiccola : imgState.TestoFirma;
|
|
|
|
|
if (string.IsNullOrEmpty(text) && _picSettings.TestoNome)
|
|
|
|
|
text = imgState.NomeFileBig;
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(text))
|
|
|
|
|
{
|
|
|
|
|
var fontSize = isThumbnail ? imgState.DimensioneStandardMiniatura : imgState.DimensioneStandard;
|
|
|
|
|
using var font = new System.Drawing.Font(_picSettings.IlFont ?? "Arial", Math.Max(6, fontSize));
|
|
|
|
|
using var shadowBrush = new SolidBrush(System.Drawing.Color.FromArgb(imgState.AlphaScelta, 0, 0, 0));
|
|
|
|
|
using var textBrush = new SolidBrush(System.Drawing.Color.FromArgb(imgState.AlphaScelta, _picSettings.FontColoreRGB));
|
|
|
|
|
|
|
|
|
|
var sf = new StringFormat { Alignment = StringAlignment.Center };
|
|
|
|
|
var x = bmp.Width / 2f;
|
|
|
|
|
var y = isThumbnail ? bmp.Height - fontSize - 4 : bmp.Height - fontSize - (_picSettings.Margine);
|
|
|
|
|
|
|
|
|
|
g.DrawString(text, font, shadowBrush, new System.Drawing.PointF(x + 1, y + 1), sf);
|
|
|
|
|
g.DrawString(text, font, textBrush, new System.Drawing.PointF(x, y), sf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw logo if provided
|
|
|
|
|
if (logo != null && _picSettings.LogoAggiungi && File.Exists(_picSettings.LogoNomeFile))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var target = new System.Drawing.Size(_picSettings.LogoLarghezza, _picSettings.LogoAltezza);
|
|
|
|
|
using var logoResized = new Bitmap(logo, target.Width, target.Height);
|
|
|
|
|
|
|
|
|
|
int xPos = _picSettings.LogoPosizioneH?.ToUpperInvariant() == "DESTRA" ? bmp.Width - target.Width - _picSettings.Margine : _picSettings.Margine;
|
|
|
|
|
int yPos = _picSettings.LogoPosizioneV?.ToUpperInvariant() == "BASSO" ? bmp.Height - target.Height - _picSettings.Margine : _picSettings.Margine;
|
|
|
|
|
|
|
|
|
|
var cm = new System.Drawing.Imaging.ColorMatrix { Matrix33 = (float)Math.Clamp((int.TryParse(_picSettings.LogoTrasparenza, out var lt) ? lt : 100) / 100.0, 0.0, 1.0) };
|
|
|
|
|
var ia = new System.Drawing.Imaging.ImageAttributes();
|
|
|
|
|
ia.SetColorMatrix(cm, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap);
|
|
|
|
|
|
|
|
|
|
g.DrawImage(logoResized, new System.Drawing.Rectangle(xPos, yPos, target.Width, target.Height), 0, 0, target.Width, target.Height, GraphicsUnit.Pixel, ia);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(ex, "[Alternate] Error drawing logo in GDI pass");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure directory
|
|
|
|
|
var dir = System.IO.Path.GetDirectoryName(outputPath);
|
|
|
|
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
|
|
|
|
|
|
|
|
|
// Save with requested quality using GDI encoder
|
|
|
|
|
var encoder = GetEncoder(ImageFormat.Jpeg);
|
|
|
|
|
var myEncoder = System.Drawing.Imaging.Encoder.Quality;
|
|
|
|
|
using var encoderParams = new System.Drawing.Imaging.EncoderParameters(1);
|
|
|
|
|
encoderParams.Param[0] = new System.Drawing.Imaging.EncoderParameter(myEncoder, quality);
|
|
|
|
|
bmp.Save(outputPath, encoder, encoderParams);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static ImageCodecInfo GetEncoder(System.Drawing.Imaging.ImageFormat format)
|
|
|
|
|
{
|
|
|
|
|
var codecs = ImageCodecInfo.GetImageDecoders();
|
|
|
|
|
foreach (var codec in codecs)
|
|
|
|
|
{
|
|
|
|
|
if (codec.FormatID == format.Guid) return codec;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 01:03:26 +01:00
|
|
|
|