Completely removed GDI
Some checks failed
Build Windows Avalonia / build (push) Failing after 1m47s
Release Windows Avalonia / build (push) Failing after 1m47s
Release Windows Avalonia / release (push) Has been skipped

This commit is contained in:
Maddo 2026-05-28 20:27:05 +02:00
commit d76e133f18
31 changed files with 236 additions and 2592 deletions

View file

@ -1,866 +0,0 @@
#if WINDOWS
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
// Imports System.Threading
namespace MaddoShared;
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> logger) : IImageCreator
{
public async Task CreateImageAsync(ImageState imgState, byte[]? logoData)
{
try
{
await Task.Run(() =>
{
logger.LogInformation("File: {FileInfo} Dest: {DirectoryInfo}", imgState.WorkFile, imgState.DestDir);
PrepareVariables(imgState);
ExtractExif(imgState);
using var g = Image.FromFile(imgState.WorkFile.FullName);
// Set extra text
SetExtraText(g, imgState);
// Rotate image according to EXIF
ApplyRotation(g, imgState);
// Force jpeg if option selected
var thisFormat = g.RawFormat;
if (picSettings.UsaForzaJpg)
thisFormat = ImageFormat.Jpeg;
PrepareThumbnailSize(g, imgState);
using var imgOutputBig = new Bitmap(g, imgState.ThumbSizeBig.Width, imgState.ThumbSizeBig.Height);
imgOutputBig.SetResolution(g.HorizontalResolution, g.VerticalResolution);
// Create thumbnails
CreateThumbnails(g, imgState, imgOutputBig, thisFormat);
AddText(g, imgState, imgOutputBig);
AddLogo(imgOutputBig, logoData);
SavePhoto(imgOutputBig, imgState, thisFormat);
}).ConfigureAwait(false);
}
catch (Exception ex)
{
var e = ex.Demystify();
logger.LogError(e, "Error in processing photo {WorkFileName}", imgState.WorkFile.Name);
}
}
private void ExtractExif(ImageState imgState)
{
using var img = SixLabors.ImageSharp.Image.Load(imgState.WorkFile.FullName);
imgState.Orientation = Orientations.TopLeft;
IExifValue<ushort> rotation = null;
var exifProfile = img.Metadata?.ExifProfile;
var found = exifProfile != null && exifProfile.TryGetValue(ExifTag.Orientation, out rotation);
if (found)
{
var intOrientation = rotation.Value.ToInt32();
imgState.Orientation = (Orientations)intOrientation;
}
IExifValue<string> date = null;
var creationFound = exifProfile != null && exifProfile.TryGetValue(ExifTag.DateTimeOriginal, out date);
if (creationFound)
{
var succ = DateTime.TryParseExact(date.Value, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var crDate);
if (succ)
{
imgState.CreationDate = crDate;
}
else
{
imgState.CreationDate = null;
}
}
else
{
imgState.CreationDate = null;
}
}
private void ApplyRotation(Image g, ImageState imgState)
{
imgState.FotoRuotaADestra = false;
imgState.FotoRuotaASinistra = false;
if (picSettings.UsaRotazioneAutomatica && g.PropertyIdList.Length > 0)
{
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:
break;
}
}
if (imgState.FotoRuotaASinistra)
g.RotateFlip(RotateFlipType.Rotate270FlipNone);
if (imgState.FotoRuotaADestra)
g.RotateFlip(RotateFlipType.Rotate90FlipNone);
}
/// <summary>
/// ''' Aggiunge Orario, tempo gara e altri
/// ''' </summary>
/// ''' <param name="g">Image</param>
/// <param name="imgState"></param>
/// ''' <remarks></remarks>
private void SetExtraText(Image g, ImageState imgState)
{
if (picSettings.UsaOrarioTestoApplicare || picSettings.UsaTempoGaraTestoApplicare ||
picSettings.UsaOrarioMiniatura || picSettings.TestoMin || picSettings.AggTempoGaraMin ||
picSettings.AggNumTempMin)
{
if (g.PropertyIdList.Length <= 0) return;
imgState.DataFoto = imgState.CreationDate ?? DateTime.Now;
imgState.TestoFirma = picSettings.TestoFirmaStart;
imgState.TestoFirmaV = picSettings.TestoFirmaStartV;
if (imgState.DataFoto.Year == 1) return;
imgState.TestoFirmaPiccola = imgState.DataFoto.ToShortTimeString();
if (picSettings.UsaOrarioTestoApplicare)
{
imgState.TestoFirma +=
$" {imgState.DataFoto.ToShortDateString()} {imgState.DataFoto.ToLongTimeString()}";
imgState.TestoFirmaV +=
$" {imgState.DataFoto.ToShortDateString()} {imgState.DataFoto.ToLongTimeString()}";
}
if (!picSettings.UsaTempoGaraTestoApplicare) return;
var diff = imgState.DataFoto - imgState.DataPartenzaI;
imgState.TestoFirma += $" {imgState.TestoOrario}{diff.Hours:00}:{diff.Minutes:00}:{diff.Seconds:00}";
imgState.TestoFirmaV += $" {imgState.TestoOrario}{diff.Hours:00}:{diff.Minutes:00}:{diff.Seconds:00}";
}
else
{
imgState.TestoFirma = picSettings.TestoFirmaStart;
imgState.TestoFirmaV = picSettings.TestoFirmaStartV;
}
}
/// <summary>
/// ''' Prepara diverse variabili azzerandole, elaborandole e prendendole dalle impostazioni
/// ''' </summary>
/// ''' <remarks></remarks>
private void PrepareVariables(ImageState imgState)
{
imgState.AlphaScelta = System.Convert.ToInt32((255 * (100 - picSettings.Trasparenza) / (double)100));
imgState.TestoFirma = "";
imgState.TestoFirmaV = "";
imgState.DataPartenzaI = picSettings.DataPartenza;
imgState.TestoOrario = picSettings.TestoOrario;
if (imgState.TestoOrario.Length > 0)
imgState.TestoOrario += " ";
imgState.TestoFirmaPiccola = "";
imgState.ThumbSizeSmall = new Size();
imgState.ThumbSizeBig = new Size();
imgState.NomeFileSmall = "";
imgState.NomeFileBig2 = "";
imgState.NomeFileBig = "";
imgState.DimensioneStandard = picSettings.DimStandard;
imgState.DimensioneStandardMiniatura = picSettings.DimStandardMiniatura;
// nomeFileSmall = Suffisso & NomeFileChild
// nomeFileBig = NomeFileChild
imgState.NomeFileSmall = picSettings.Suffisso + imgState.WorkFile.Name;
imgState.NomeFileBig = imgState.WorkFile.Name;
// Sanitize file names to avoid invalid characters causing IO errors
imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall);
imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig);
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return fileName;
var invalid = 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 PrepareThumbnailSize(Image g, ImageState imgState)
{
if (g.Width > g.Height)
{
imgState.ThumbSizeSmall = CalculateThumbnailSize(g.Width, g.Height, picSettings.LarghezzaSmall, "Larghezza");
var sizeOrig = new Size(g.Width, g.Height);
imgState.ThumbSizeBig = sizeOrig;
}
else
{
imgState.ThumbSizeSmall = CalculateThumbnailSize(g.Width, g.Height, picSettings.AltezzaSmall, "Altezza");
var sizeOrig = new Size(g.Width, g.Height);
imgState.ThumbSizeBig = sizeOrig;
}
}
private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format)
{
// Only skip thumbnail generation when the global "create thumbnails" flag is false.
// Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText
if (!picSettings.CreaMiniature)
return;
PrepareSignatureText(imgState);
if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione))
UpdateFilenameWithCode(imgState);
if (ShouldRenderText())
CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format);
else
CreateSimpleThumbnail(sourceImage, imgState, format);
}
private void PrepareSignatureText(ImageState imgState)
{
if (picSettings.TestoMin)
imgState.TestoFirmaPiccola = imgState.NomeFileBig;
else if (picSettings.AggNumTempMin)
imgState.TestoFirmaPiccola = imgState.NomeFileBig + " ";
}
private bool IsSameDirectory(string dir1, string dir2) =>
string.Equals(dir1, dir2, StringComparison.OrdinalIgnoreCase);
private void UpdateFilenameWithCode(ImageState imgState)
{
var name = imgState.NomeFileSmall;
imgState.NomeFileSmall = name[..^4] + picSettings.Codice + name[^4..];
}
private bool ShouldRenderText() =>
picSettings.UsaOrarioMiniatura || picSettings.TestoMin || picSettings.AggTempoGaraMin ||
picSettings.AggNumTempMin;
private void CreateSimpleThumbnail(Image image, ImageState imgState, ImageFormat format)
{
using var thumbnail = new Bitmap(image, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height);
thumbnail.Save(Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall), format);
}
private void CreateThumbnailWithText(Image image, ImageState imgState, Bitmap sourceBitmap, ImageFormat format)
{
if (imgState.TestoFirmaPiccola.Length == 0)
{
CreateSimpleThumbnail(image, imgState, format);
return;
}
using var imgOutputSmall = (Bitmap)sourceBitmap.Clone();
using var graphics = Graphics.FromImage(imgOutputSmall);
graphics.SmoothingMode = SmoothingMode.AntiAlias;
// Use the user's configured font size directly
using var font1 = CreateFont(picSettings.IlFont, imgState.DimensioneStandardMiniatura, picSettings.Grassetto);
var textSize = graphics.MeasureString(imgState.TestoFirmaPiccola, font1);
// Adjust font if it's too large for the image dimensions
// Keep text height under 15% of image height to ensure proper spacing when resized
// This leaves room for margins and prevents clipping
int tempFontSize = imgState.DimensioneStandardMiniatura;
float maxTextHeight = image.Height * 0.15f;
while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5)
{
tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1;
using var tempFont = CreateFont(picSettings.IlFont, tempFontSize, picSettings.Grassetto);
textSize = graphics.MeasureString(imgState.TestoFirmaPiccola, tempFont);
}
// Re-measure text with the final font size for accurate positioning
using var finalFont = CreateFont(picSettings.IlFont, tempFontSize, picSettings.Grassetto);
var finalTextSize = graphics.MeasureString(imgState.TestoFirmaPiccola, finalFont);
SetVerticalPosition(image.Height, finalTextSize.Height, imgState);
float xCenter = CalculateHorizontalAlignment(image.Width, finalTextSize.Width);
using var stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
using var shadowBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, 0, 0, 0));
using var textBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, picSettings.FontColoreRGB));
DrawText(graphics, imgState, xCenter, stringFormat, shadowBrush, textBrush, finalFont);
using var finalThumb =
new Bitmap(imgOutputSmall, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height);
finalThumb.Save(Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall), format);
}
private Font CreateFont(string fontName, int size, bool bold) =>
new Font(fontName, size, bold ? FontStyle.Bold : FontStyle.Regular);
private int FindBestFontSize(Graphics g, string text, string fontName, int maxSize, bool bold, int maxWidth, int minSize = 5)
{
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;
using var testFont = CreateFont(fontName, mid, bold);
var measured = g.MeasureString(text, testFont);
if (measured.Width <= maxWidth)
{
best = mid;
low = mid + 1; // try larger
}
else
{
high = mid - 1; // too big
}
}
return best;
}
private void AddText(Image g, ImageState imgState, Bitmap imgOutputBig)
{
using var grPhoto = Graphics.FromImage(imgOutputBig);
grPhoto.SmoothingMode = SmoothingMode.AntiAlias;
// Determine best base font size using a binary search (faster than decremental loop)
int availableWidth = (int)g.Width;
int targetBaseSize = imgState.DimensioneStandard > 0 ? imgState.DimensioneStandard : picSettings.DimStandard;
int bestBaseSize = FindBestFontSize(grPhoto, imgState.TestoFirma ?? string.Empty, picSettings.IlFont, targetBaseSize, picSettings.Grassetto, availableWidth);
imgState.DimensioneStandard = bestBaseSize;
// Decide final drawing size (use DimVert if rotated)
int drawSize = (imgState.FotoRuotaADestra || imgState.FotoRuotaASinistra) ? picSettings.DimVert : imgState.DimensioneStandard;
using var drawFont = CreateFont(picSettings.IlFont, drawSize, picSettings.Grassetto);
var crSize = grPhoto.MeasureString(imgState.TestoFirma ?? string.Empty, drawFont);
var larghezzaStandard = Convert.ToInt32(crSize.Width);
// Vertical positions
switch (picSettings.Posizione.ToUpper())
{
case "ALTO":
{
imgState.YPosFromBottom = picSettings.Margine;
imgState.YPosFromBottom3 = picSettings.MargVert;
break;
}
case "BASSO":
{
imgState.YPosFromBottom =
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0)));
imgState.YPosFromBottom3 =
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0)));
break;
}
}
float xCenterOfImg = 0;
using var strFormat = new StringFormat();
switch (picSettings.Allineamento.ToUpper())
{
case "SINISTRA":
{
xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2)));
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
break;
}
case "CENTRO":
{
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
break;
}
case "DESTRA":
{
xCenterOfImg =
Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2)));
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
break;
}
}
strFormat.Alignment = StringAlignment.Center;
using var semiTransBrush2 = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, 0, 0, 0));
using var semiTransBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, picSettings.FontColoreRGB));
// write text (NomeFileBig)
if (picSettings.TestoNome)
{
if (picSettings.NomeData && g.PropertyIdList.Length > 0)
{
imgState.DataFoto = imgState.CreationDate ?? DateTime.Now;
grPhoto.DrawString((imgState.NomeFileBig + " " + imgState.DataFoto.ToShortDateString()), drawFont,
semiTransBrush2, new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat);
grPhoto.DrawString((imgState.NomeFileBig + " " + imgState.DataFoto.ToShortDateString()), drawFont,
semiTransBrush, new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat);
}
else
{
grPhoto.DrawString(imgState.NomeFileBig, drawFont, semiTransBrush2,
new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat);
grPhoto.DrawString(imgState.NomeFileBig, drawFont, semiTransBrush,
new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat);
}
}
else
{
if (imgState.FotoRuotaADestra || imgState.FotoRuotaASinistra)
{
if (!picSettings.TestoMin)
{
grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush2,
new PointF(xCenterOfImg + 1, imgState.YPosFromBottom3 + 1), strFormat);
grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush,
new PointF(xCenterOfImg, imgState.YPosFromBottom3), strFormat);
}
if (picSettings.TestoMin)
{
grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush2,
new PointF(xCenterOfImg + 1, imgState.YPosFromBottom4 + 1), strFormat);
grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush,
new PointF(xCenterOfImg, imgState.YPosFromBottom4), strFormat);
}
}
else
{
grPhoto.DrawString(imgState.TestoFirma, drawFont, semiTransBrush2,
new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat);
grPhoto.DrawString(imgState.TestoFirma, drawFont, semiTransBrush,
new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat);
}
}
if (string.Equals(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione,
StringComparison.OrdinalIgnoreCase))
{
imgState.NomeFileBig2 = imgState.NomeFileBig;
imgState.NomeFileBig = $"{imgState.NomeFileBig[..^4]}{picSettings.Codice}{imgState.NomeFileBig[^4..]}";
}
}
private void AddLogo(Bitmap imgOutputBig, byte[]? logoData)
{
// Skip if no logo bytes provided
if (logoData is null) return;
if (!picSettings.LogoAggiungi) return;
using var logo = Image.FromStream(new System.IO.MemoryStream(logoData));
// Decide whether to apply a color-key transparency remap or rely on existing image alpha.
// If UseTransparentColor is true, parse the configured TransparentColor and remap it to fully transparent.
using var grWatermark = Graphics.FromImage(imgOutputBig);
using ImageAttributes imageAttributes = new ImageAttributes();
if (picSettings.UseTransparentColor)
{
Color keyColor = Color.White;
try
{
if (!string.IsNullOrWhiteSpace(picSettings.TransparentColor))
{
// ColorTranslator accepts both "#RRGGBB" and "RRGGBB"
keyColor = ColorTranslator.FromHtml(picSettings.TransparentColor);
}
}
catch
{
keyColor = Color.White;
}
var colorMap = new ColorMap
{
// background: the color we search for and replace with transparency
OldColor = keyColor,
NewColor = Color.FromArgb(0, 0, 0, 0)
};
var remapTable = new[] { colorMap };
imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap);
}
// * The second color manipulation is used to change the opacity by setting the 3rd row and 3rd column to 0.3f
// Parse transparency safely (default to 100 if parsing fails)
if (!int.TryParse(picSettings.LogoTrasparenza, out var logoTransparencyValue))
{
logoTransparencyValue = 100;
}
var colorMatrixElements = new[]
{
new[] { 1.0F, 0.0F, 0.0F, 0.0F, 0.0F }, new[] { 0.0F, 1.0F, 0.0F, 0.0F, 0.0F },
new[] { 0.0F, 0.0F, 1.0F, 0.0F, 0.0F },
new[] { 0.0F, 0.0F, 0.0F, System.Convert.ToSingle(logoTransparencyValue) / 100F, 0.0F },
new[] { 0.0F, 0.0F, 0.0F, 0.0F, 1.0F }
};
var wmColorMatrix = new ColorMatrix(colorMatrixElements);
imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
var fotoLogoH = picSettings.LogoAltezza;
var fotoLogoW = picSettings.LogoLarghezza;
var fattoreAlt = logo.Height / (double)fotoLogoH;
var fattoreLarg = logo.Width / (double)fotoLogoW;
var nuovaSize = fattoreLarg > fattoreAlt
? CalculateThumbnailSize(logo.Width, logo.Height, fotoLogoW, "Larghezza")
: CalculateThumbnailSize(logo.Width, logo.Height, fotoLogoH, "Altezza");
// Guard against null/empty LogoMargine and percentage parsing
var logoMargineStr = picSettings.LogoMargine ?? string.Empty;
var inPercentualeL = logoMargineStr.EndsWith('%');
var margineL = 0;
if (inPercentualeL)
{
var trimmed = logoMargineStr.TrimEnd('%');
if (!int.TryParse(trimmed, out margineL)) margineL = 0;
}
else
{
if (!int.TryParse(logoMargineStr, out margineL)) margineL = 0;
}
var margineUsato =
inPercentualeL ? System.Convert.ToInt32(imgOutputBig.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 = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2);
break;
}
case "DESTRA":
{
xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato);
break;
}
}
switch (logoV)
{
case "ALTO":
case "NESSUNA":
{
yPosOfWm = margineUsato;
break;
}
case "CENTRO":
{
yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2);
break;
}
case "BASSO":
{
yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato);
break;
}
}
grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0,
logo.Width, logo.Height, GraphicsUnit.Pixel, imageAttributes);
//grWatermark.Dispose();
}
private void SavePhoto(Bitmap imgOutputBig, ImageState imgState, ImageFormat thisFormat)
{
var fileName = Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig);
using var image1Stream = new MemoryStream();
if (picSettings.FotoGrandeDimOrigina == false)
{
// attenzione non controlla se è png
// imgOutputBig.Save(Path.Combine(_DestDir.FullName, "Temp_" & NomeFileBig), thisFormat)
if (thisFormat.Equals(ImageFormat.Jpeg))
{
MakeImageCustomQuality(imgOutputBig, image1Stream, picSettings.JpegQuality);
}
//SalvaImmagineCustomQuality(imgOutputBig, Path.Combine(DestDir.FullName, "Temp_" + NomeFileBig), _picSettings.jpegQuality);
else
{
imgOutputBig.Save(image1Stream, thisFormat);
}
//imgOutputBig.Save(Path.Combine(DestDir.FullName, "Temp_" + NomeFileBig), thisFormat);
image1Stream.Seek(0, SeekOrigin.Begin);
using var g2 = Image.FromStream(image1Stream);
imgState.ThumbSizeBig = g2.Width > g2.Height
? CalculateThumbnailSize(g2.Width, g2.Height, picSettings.LarghezzaBig, "Larghezza")
: CalculateThumbnailSize(g2.Width, g2.Height, picSettings.AltezzaBig, "Altezza");
using var imgOutputBig2 = new Bitmap(g2, imgState.ThumbSizeBig.Width, imgState.ThumbSizeBig.Height);
if (!picSettings.OverwriteFiles && File.Exists(fileName))
{
logger.LogInformation("Saltata foto {FileName}, esiste", fileName);
}
else
{
if (thisFormat.Equals(ImageFormat.Jpeg))
SaveImageCustomQuality(imgOutputBig2, fileName, picSettings.JpegQuality);
else
imgOutputBig2.Save(fileName, thisFormat);
}
}
else
{
if (!picSettings.OverwriteFiles && File.Exists(fileName))
{
logger.LogInformation("Saltata foto {FileName}, esiste", fileName);
}
else
{
if (thisFormat.Equals(ImageFormat.Jpeg))
SaveImageCustomQuality(imgOutputBig, fileName, picSettings.JpegQuality);
else
imgOutputBig.Save(fileName, thisFormat);
}
}
image1Stream.Seek(0, SeekOrigin.Begin);
if (!picSettings.CreaMiniature) return;
if (!picSettings.AggiungiScritteMiniature) return;
using var g1 = picSettings.FotoGrandeDimOrigina ? (Image)imgOutputBig.Clone() : Image.FromStream(image1Stream);
using var imgOutputSmall = new Bitmap(g1, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height);
if (string.Equals(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione,
StringComparison.OrdinalIgnoreCase))
imgState.NomeFileSmall = imgState.NomeFileSmall.Substring(0, imgState.NomeFileSmall.Length - 4) +
picSettings.Codice +
imgState.NomeFileSmall.Substring(imgState.NomeFileSmall.Length - 4);
var tnFileName = Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall);
if (!picSettings.OverwriteFiles && File.Exists(tnFileName))
{
logger.LogInformation("Saltata miniatura foto {TnFileName}, esiste", tnFileName);
}
else
{
if (thisFormat.Equals(ImageFormat.Jpeg))
SaveImageCustomQuality(imgOutputSmall, tnFileName, picSettings.JpegQualityMin);
else
imgOutputSmall.Save(tnFileName, thisFormat);
}
}
private void SaveImageCustomQuality(Bitmap imageToSave, string nomeFileFinale, long quality)
{
var jgpEncoder = GetEncoder(ImageFormat.Jpeg);
var myEncoder = System.Drawing.Imaging.Encoder.Quality;
using var myEncoderParameters = new EncoderParameters(1);
var myEncoderParameter = new EncoderParameter(myEncoder, quality);
myEncoderParameters.Param[0] = myEncoderParameter;
imageToSave.Save(nomeFileFinale, jgpEncoder, myEncoderParameters);
//imageToSave.Dispose();
}
private void MakeImageCustomQuality(Bitmap imageToSave, Stream destinationStream, long quality)
{
var jgpEncoder = GetEncoder(ImageFormat.Jpeg);
var myEncoder = System.Drawing.Imaging.Encoder.Quality;
using var myEncoderParameters = new EncoderParameters(1);
var myEncoderParameter = new EncoderParameter(myEncoder, quality);
myEncoderParameters.Param[0] = myEncoderParameter;
destinationStream.Seek(0, SeekOrigin.Begin);
imageToSave.Save(destinationStream, jgpEncoder, myEncoderParameters);
//imageToSave.Dispose();
}
private ImageCodecInfo GetEncoder(ImageFormat format)
{
var codecs = ImageCodecInfo.GetImageDecoders();
foreach (var codec in codecs)
{
if (codec.FormatID == format.Guid)
return codec;
}
return null /* TODO Change to default(_) if this is not a reference type */;
}
/// <summary>
/// ''' Calculate the Size of the New image
/// ''' </summary>
/// ''' <param name="currentwidth">Larghezza</param>
/// ''' <param name="currentheight">Altezza</param>
/// ''' <param name="maxPixel"></param>
/// ''' <param name="tipoSize"></param>
/// ''' <returns></returns>
/// ''' <remarks></remarks>
private Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize)
{
// e
// *** Larghezza, Altezza, Auto
double tempMultiplier;
if (tipoSize.ToUpper() == "Larghezza".ToUpper())
tempMultiplier = maxPixel / (double)currentwidth;
else if (tipoSize.ToUpper() == "Altezza".ToUpper())
tempMultiplier = maxPixel / (double)currentheight;
else if (currentheight > currentwidth)
tempMultiplier = maxPixel / (double)currentheight;
else
tempMultiplier = maxPixel / (double)currentwidth;
var newSize = new Size(System.Convert.ToInt32(currentwidth * tempMultiplier),
System.Convert.ToInt32(currentheight * tempMultiplier));
return newSize;
}
private void SetVerticalPosition(int imgHeight, float textHeight, ImageState imgState)
{
// Use 1% of image height as minimum margin, or 10px, whichever is larger
float minMargin = Math.Max(10f, imgHeight * 0.01f);
switch (picSettings.Posizione.ToUpper())
{
case "ALTO":
imgState.YPosFromBottom1 = Math.Max(minMargin, picSettings.Margine);
imgState.YPosFromBottom4 = Math.Max(minMargin, picSettings.MargVert);
break;
case "BASSO":
var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0);
var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0);
// Position from bottom: bottom edge of text at desired margin from bottom
// Y = imageHeight - textHeight - bottomMargin
var desiredY1 = imgHeight - textHeight - bottomMargin1;
var desiredY4 = imgHeight - textHeight - bottomMargin4;
// Ensure text stays completely within bounds:
// - Top edge must be >= minMargin (not clipped at top)
// - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom)
var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin
var maxAllowedY4 = imgHeight - textHeight - minMargin;
imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1));
imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4));
break;
case "CENTRO":
default:
// Center the text vertically
var centeredY = (imgHeight - textHeight) / 2f;
// Clamp to ensure margins are respected
imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(centeredY, imgHeight - textHeight - minMargin));
imgState.YPosFromBottom4 = imgState.YPosFromBottom1;
break;
}
}
private float CalculateHorizontalAlignment(int imgWidth, float textWidth)
{
double halfWidth = textWidth / 2.0;
return picSettings.Allineamento.ToUpper() switch
{
"SINISTRA" => (float)Math.Min(picSettings.Margine + halfWidth, imgWidth / 2.0),
"DESTRA" => (float)Math.Max(imgWidth - picSettings.Margine - halfWidth, imgWidth / 2.0),
_ => imgWidth / 2.0f, // CENTRO or default
};
}
private void DrawText(Graphics g, ImageState imgState, float x, StringFormat format,
Brush shadowBrush, Brush textBrush, Font font)
{
string content = imgState.TestoFirmaPiccola;
if (picSettings.TestoMin)
{
content = imgState.NomeFileBig;
}
else if (picSettings.AggTempoGaraMin && picSettings.UsaTempoGaraTestoApplicare)
{
content = FormatTimeText(imgState, includeFileName: false);
}
else if (picSettings.AggNumTempMin)
{
content = FormatTimeText(imgState, includeFileName: true);
}
var offset = new PointF(x + 1, imgState.YPosFromBottom1 + 1);
var actual = new PointF(x, imgState.YPosFromBottom1);
g.DrawString(content, font, shadowBrush, offset, format);
g.DrawString(content, font, textBrush, actual, format);
}
private string FormatTimeText(ImageState imgState, bool includeFileName)
{
var diff = imgState.DataPartenzaI - imgState.DataFoto;
var ticks = (long)(diff.TotalSeconds * 10000000);
var time = new TimeSpan(ticks);
var formatted = $"{imgState.TestoOrario}{time:hh\\:mm\\:ss}";
return includeFileName
? $"{imgState.NomeFileBig}{Environment.NewLine}{formatted}"
: Environment.NewLine + formatted;
}
}
#endif // WINDOWS

View file

@ -1,7 +1,6 @@
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;
@ -17,11 +16,8 @@ using SixLabors.ImageSharp.Drawing;
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.
/// Image creator implemented with SixLabors.ImageSharp for loading, EXIF orientation,
/// resizing, text/logo drawing, and saving.
/// </summary>
public class ImageCreatorImageSharp : IImageCreator
{
@ -38,12 +34,11 @@ public class ImageCreatorImageSharp : IImageCreator
{
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);
_logger.LogInformation("[ImageSharp] Processing {File} -> {Dest}", imgState.WorkFile?.FullName, imgState.DestDir?.FullName);
using var fs = File.OpenRead(imgState.WorkFile.FullName);
@ -59,9 +54,6 @@ public class ImageCreatorImageSharp : IImageCreator
// 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);
@ -71,10 +63,10 @@ public class ImageCreatorImageSharp : IImageCreator
// Ensure destination exists
imgState.DestDir?.Create();
var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig);
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);
await DrawAndSaveAsync(imgBig, fileNameBig, imgState, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false);
// Create thumbnail if requested
if (_picSettings.CreaMiniature)
@ -85,20 +77,16 @@ public class ImageCreatorImageSharp : IImageCreator
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);
await DrawAndSaveAsync(imgSmall, fileNameSmall, imgState, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Error processing image {File}", imgState.WorkFile?.Name);
_logger.LogError(ex, "[ImageSharp] 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);
@ -110,7 +98,7 @@ public class ImageCreatorImageSharp : IImageCreator
};
}
private async Task DrawAndSaveWithGdiAsync(Image<Rgba32> imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail)
private async Task DrawAndSaveAsync(Image<Rgba32> 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.
@ -125,13 +113,13 @@ public class ImageCreatorImageSharp : IImageCreator
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "[Alternate] Failed to clear EXIF orientation on working image");
_logger?.LogDebug(ex, "[ImageSharp] 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
// Ensure thumbnail text is prepared before drawing.
if (isThumbnail)
{
if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola))
@ -285,8 +273,7 @@ public class ImageCreatorImageSharp : IImageCreator
});
}
// 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).
// Draw logos only on full-size images.
if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail)
{
try
@ -414,7 +401,7 @@ public class ImageCreatorImageSharp : IImageCreator
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Invalid transparent color setting {Color}", _picSettings.TransparentColor);
_logger.LogError(ex, "[ImageSharp] Invalid transparent color setting {Color}", _picSettings.TransparentColor);
}
}
@ -429,7 +416,7 @@ public class ImageCreatorImageSharp : IImageCreator
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Error drawing logo in ImageSharp pass");
_logger.LogError(ex, "[ImageSharp] Error drawing logo");
}
}
@ -444,8 +431,6 @@ public class ImageCreatorImageSharp : IImageCreator
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;
@ -454,7 +439,6 @@ public class ImageCreatorImageSharp : IImageCreator
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
@ -517,9 +501,7 @@ public class ImageCreatorImageSharp : IImageCreator
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)
// Set rotation flags on the state so other code can pick the correct
// text variant (vertical vs horizontal). Mirror ImageCreatorSharp logic.
// Set rotation flags on the state so other code can pick the correct text variant.
imgState.FotoRuotaADestra = false;
imgState.FotoRuotaASinistra = false;
@ -567,11 +549,11 @@ public class ImageCreatorImageSharp : IImageCreator
catch (Exception ex)
{
// Non-fatal: log and continue
_logger?.LogDebug(ex, "[Alternate] Could not clear EXIF orientation tag");
_logger?.LogDebug(ex, "[ImageSharp] Could not clear EXIF orientation tag");
}
}
private System.Drawing.Size ComputeBigSize(int width, int height)
private Size ComputeBigSize(int width, int height)
{
// If original large size option requested, return original
// otherwise compute based on width/height limits from settings
@ -580,16 +562,14 @@ public class ImageCreatorImageSharp : IImageCreator
: CalculateThumbnailSize(width, height, _picSettings.AltezzaBig, "Altezza");
}
private System.Drawing.Size ComputeSmallSize(int width, int height)
private 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)
private static Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize)
{
double tempMultiplier;
if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase))
@ -601,7 +581,7 @@ public class ImageCreatorImageSharp : IImageCreator
else
tempMultiplier = maxPixel / (double)currentwidth;
var newSize = new System.Drawing.Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier));
var newSize = new Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier));
return newSize;
}

View file

@ -1,54 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MaddoShared;
/// <summary>
/// Dynamically resolves the concrete IImageCreator implementation at call time
/// based on current PicSettings.ImageCreatorProvider.
/// On non-Windows platforms only ImageCreatorImageSharp is available.
/// </summary>
public class ImageCreatorMapper : IImageCreator
{
private readonly IServiceProvider _sp;
private readonly PicSettings _settings;
private readonly ILogger<ImageCreatorMapper> _logger;
public ImageCreatorMapper(IServiceProvider sp, PicSettings settings, ILogger<ImageCreatorMapper> logger)
{
_sp = sp ?? throw new ArgumentNullException(nameof(sp));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_logger = logger;
}
public Task CreateImageAsync(ImageState imgState, byte[]? logoData)
{
var provider = (_settings.ImageCreatorProvider ?? "Sharp").Trim();
_logger?.LogDebug("Resolving IImageCreator for provider '{Provider}'", provider);
#if WINDOWS
return provider.Equals("ALTERNATE", StringComparison.OrdinalIgnoreCase)
? ResolveAndCall<ImageCreatorImageSharp>(imgState, logoData)
: ResolveAndCall<ImageCreatorGDI>(imgState, logoData);
#else
// GDI is not available on non-Windows — always use ImageSharp
return ResolveAndCall<ImageCreatorImageSharp>(imgState, logoData);
#endif
}
private Task ResolveAndCall<T>(ImageState imgState, byte[]? logoData) where T : IImageCreator
{
var impl = (IImageCreator?)_sp.GetService(typeof(T));
if (impl is null)
{
_logger?.LogWarning("Requested image creator {Type} is not registered. Falling back to ImageCreatorImageSharp.", typeof(T).Name);
impl = (IImageCreator?)_sp.GetService(typeof(ImageCreatorImageSharp));
}
if (impl is null)
throw new InvalidOperationException("No IImageCreator implementation is registered.");
return impl.CreateImageAsync(imgState, logoData);
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Drawing;
using System.IO;
namespace MaddoShared;
@ -28,18 +27,16 @@ public class ImageState
public DateTime DataPartenzaI { get; set; }
public string TestoOrario { get; set; }
public string TestoFirmaPiccola { get; set; }
public Size ThumbSizeSmall { get; set; }
public Size ThumbSizeBig { get; set; }
public string NomeFileSmall{ get; set; }
public string NomeFileBig{ get; set; }
public string NomeFileBig2{ get; set; }
public string NomeFileSmall { get; set; }
public string NomeFileBig { get; set; }
public string NomeFileBig2 { get; set; }
public float YPosFromBottom{ get; set; }
public float YPosFromBottom1{ get; set; }
public float YPosFromBottom2{ get; set; }
public float YPosFromBottom3{ get; set; }
public float YPosFromBottom4{ get; set; }
public float YPosFromBottom { get; set; }
public float YPosFromBottom1 { get; set; }
public float YPosFromBottom2 { get; set; }
public float YPosFromBottom3 { get; set; }
public float YPosFromBottom4 { get; set; }
public Orientations Orientation{ get; set; }
public DateTime? CreationDate{ get; set; }
public Orientations Orientation { get; set; }
public DateTime? CreationDate { get; set; }
}

View file

@ -1,15 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net10.0-windows</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<PlatformTarget>x64</PlatformTarget>
<EnableMaddoSharedGdi Condition="'$(EnableMaddoSharedGdi)' == '' and '$(TargetFramework)' == 'net10.0-windows'">true</EnableMaddoSharedGdi>
<EnableMaddoSharedGdi Condition="'$(EnableMaddoSharedGdi)' == ''">false</EnableMaddoSharedGdi>
<!-- WINDOWS preprocessor symbol enables the optional GDI image creator. -->
<DefineConstants Condition="'$(EnableMaddoSharedGdi)' == 'true'">$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncEnumerator" Version="4.0.2" />
@ -33,6 +29,5 @@
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.3" Condition="'$(EnableMaddoSharedGdi)' == 'true'" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,6 @@
using System;
using System.Drawing;
using System.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace MaddoShared;
@ -35,7 +35,7 @@ public class PicSettings
public int Margine { get; set; }
public int LogoAltezza { get; set; }
public int LogoLarghezza { get; set; }
public Color FontColoreRGB { get; set; }
public Rgba32 FontColoreRGB { get; set; } = new(255, 255, 0, 255);
public bool LogoAggiungi { get; set; }
// Initialize logo-related strings to safe defaults to avoid null reference issues
public string LogoNomeFile { get; set; } = string.Empty;
@ -81,6 +81,4 @@ public class PicSettings
public bool FotoRuotaASinistra { get; set; } = false;
public string TempMinText { get; set; } = string.Empty;
public bool OverwriteFiles { get; set; } = false;
// Which image creator to use: "Sharp" for current implementation, "Alternate" for alternate library
public string ImageCreatorProvider { get; set; } = "Sharp";
}