Catalog/MaddoShared/ImageCreatorAlternate.cs
MaddoScientisto 63751af18d Add selectable image library option and refactor processing
Introduce UI option to choose between System.Graphics and ImageSharp for image processing. Update DataModel and MainForm for robust binding and synchronization. Rewrite ImageCreatorAlternate to use ImageSharp for core operations and GDI+ for overlays. Remove test buttons, add radio group for library selection. Update project dependencies to support new features and modernize image handling.
2026-02-15 01:03:26 +01:00

301 lines
13 KiB
C#

using System;
using System.IO;
using System.Threading.Tasks;
using System.Drawing;
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 System.Drawing.Imaging;
namespace MaddoShared;
/// <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>
public class ImageCreatorAlternate : IImageCreator
{
private readonly PicSettings _picSettings;
private readonly ILogger<ImageCreatorAlternate> _logger;
public ImageCreatorAlternate(PicSettings picSettings, ILogger<ImageCreatorAlternate> logger)
{
_picSettings = picSettings ?? throw new ArgumentNullException(nameof(picSettings));
_logger = logger;
}
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)
{
// 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;
}
}