Add color-key transparency support for logo overlays

- Allow users to select a transparent color for logo images (watermarks) via UI (checkbox, color picker, preview).
- Apply color-key transparency in both System.Drawing and ImageSharp backends: specified color in logo is made fully transparent.
- Persist transparency settings in PicSettings and SettingsDto; bind to DataModel and UI controls.
- Update logo preview to reflect transparency in real time.
- Add option to select image processing library (System.Drawing or ImageSharp) in UI and settings.
- Fix bug in SettingsService parameter loading for int/double/DateTime.
- Fully integrate color-key transparency into image processing and settings serialization.
This commit is contained in:
MaddoScientisto 2026-02-15 11:13:23 +01:00
commit 8872080741
9 changed files with 732 additions and 168 deletions

View file

@ -1,7 +1,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Drawing;
// System.Drawing not required for ImageSharp-based drawing in this class
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -10,7 +10,9 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using System.Linq;
using System.Drawing.Imaging;
using SixLabors.Fonts;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Drawing;
namespace MaddoShared;
@ -68,17 +70,9 @@ public class ImageCreatorAlternate : IImageCreator
var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig);
// Draw overlays (text/logo) onto big image via GDI+ and save
// Draw overlays (text/logo) onto big image using ImageSharp 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)
{
@ -87,7 +81,7 @@ public class ImageCreatorAlternate : IImageCreator
var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall);
// Draw overlays and save thumbnail via GDI+
// Draw overlays and save thumbnail via ImageSharp
await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logo, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false);
}
}
@ -115,14 +109,28 @@ public class ImageCreatorAlternate : IImageCreator
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);
// 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();
using var bmp = new Bitmap(ms);
using var g = Graphics.FromImage(bmp);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// Ensure DataFoto is set (extracted earlier) so time-based text is available
imgState.DataFoto = imgState.CreationDate ?? DateTime.Now;
// Ensure thumbnail text is prepared similarly to ImageCreatorSharp logic
if (isThumbnail)
{
if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola))
{
if (_picSettings.TestoMin)
imgState.TestoFirmaPiccola = imgState.NomeFileBig;
else if (_picSettings.AggNumTempMin)
imgState.TestoFirmaPiccola = imgState.NomeFileBig + " ";
else if (_picSettings.UsaOrarioMiniatura)
imgState.TestoFirmaPiccola = imgState.DataFoto.ToShortTimeString();
else
imgState.TestoFirmaPiccola ??= string.Empty;
}
}
// Prepare text
var text = isThumbnail ? imgState.TestoFirmaPiccola : imgState.TestoFirma;
@ -131,39 +139,251 @@ public class ImageCreatorAlternate : IImageCreator
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));
// Choose target starting font size
var baseSize = isThumbnail ? imgState.DimensioneStandardMiniatura : imgState.DimensioneStandard;
var sf = new StringFormat { Alignment = StringAlignment.Center };
var x = bmp.Width / 2f;
var y = isThumbnail ? bmp.Height - fontSize - 4 : bmp.Height - fontSize - (_picSettings.Margine);
// 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));
}
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);
// Color with alpha
var textColor = SixLabors.ImageSharp.Color.Black;
try
{
var c = _picSettings.FontColoreRGB;
textColor = SixLabors.ImageSharp.Color.FromRgba(c.R, c.G, c.B, (byte)imgState.AlphaScelta);
}
catch
{
textColor = SixLabors.ImageSharp.Color.Black;
}
var shadowColor = SixLabors.ImageSharp.Color.FromRgba(0, 0, 0, (byte)imgState.AlphaScelta);
// Find best font size so text fits image width (allow 95% width) and not exceed ~15% height
var maxTextWidth = working.Width * 0.95f;
var maxTextHeight = working.Height * 0.15f;
var chosenSize = FindBestFontSize(text, font.Name, Math.Max(6, baseSize), maxTextWidth, maxTextHeight);
// Use final font
var finalFont = SixLabors.Fonts.SystemFonts.CreateFont(font.Name, chosenSize);
if (!isThumbnail) imgState.DimensioneStandard = (int)Math.Round(chosenSize);
// Approximate measured size since TextMeasurer/RendererOptions may not be available in this Fonts version
var approxWidth = finalFont.Size * text.Length * 0.6f;
var approxHeight = finalFont.Size * 1.0f;
// Compute horizontal position based on alignment
float xCenterOfImg;
switch ((_picSettings.Allineamento ?? string.Empty).ToUpperInvariant())
{
case "SINISTRA":
xCenterOfImg = Math.Min(_picSettings.Margine + (approxWidth / 2f), working.Width / 2f);
break;
case "DESTRA":
xCenterOfImg = Math.Max(working.Width - _picSettings.Margine - (approxWidth / 2f), working.Width / 2f);
break;
default:
xCenterOfImg = working.Width / 2f;
break;
}
// Compute vertical position similar to ImageCreatorSharp behaviour
float originY;
var pos = (_picSettings.Posizione ?? string.Empty).ToUpperInvariant();
if (pos == "ALTO")
{
originY = _picSettings.Margine;
}
else if (pos == "BASSO")
{
originY = (float)(working.Height - approxHeight - (working.Height * _picSettings.Margine / 100.0));
}
else
{
originY = (working.Height - approxHeight) / 2f;
}
// Compute origin X so the text is centered around xCenterOfImg
var originX = xCenterOfImg - approxWidth / 2f;
// Clamp origin so text remains inside the image bounds
if (approxWidth > working.Width)
{
approxWidth = working.Width * 0.95f;
}
if (approxHeight > working.Height)
{
approxHeight = working.Height * 0.9f;
}
originX = Math.Max(0, Math.Min(originX, working.Width - approxWidth));
originY = Math.Max(0, Math.Min(originY, working.Height - approxHeight));
// Draw shadow then text
working.Mutate(ctx =>
{
ctx.DrawText(text, finalFont, shadowColor, new SixLabors.ImageSharp.PointF(originX + 1, originY + 1));
ctx.DrawText(text, finalFont, textColor, new SixLabors.ImageSharp.PointF(originX, originY));
});
}
// Draw logo if provided
if (logo != null && _picSettings.LogoAggiungi && File.Exists(_picSettings.LogoNomeFile))
if (logo != null && _picSettings.LogoAggiungi)
{
try
{
var target = new System.Drawing.Size(_picSettings.LogoLarghezza, _picSettings.LogoAltezza);
using var logoResized = new Bitmap(logo, target.Width, target.Height);
Image<Rgba32> logoImg = null;
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;
// Prefer configured file if present, otherwise use the provided System.Drawing.Image instance
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
{
// Convert System.Drawing.Image to ImageSharp by saving to PNG in-memory to preserve alpha
await using var ms = new MemoryStream();
logo.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);
logoImg = await SixLabors.ImageSharp.Image.LoadAsync<Rgba32>(ms).ConfigureAwait(false);
}
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);
var fotoLogoH = _picSettings.LogoAltezza;
var fotoLogoW = _picSettings.LogoLarghezza;
g.DrawImage(logoResized, new System.Drawing.Rectangle(xPos, yPos, target.Width, target.Height), 0, 0, target.Width, target.Height, GraphicsUnit.Pixel, ia);
// When rendering on thumbnails, limit logo size relative to the thumbnail dimensions
var effectiveMaxLogoWidth = fotoLogoW;
var effectiveMaxLogoHeight = fotoLogoH;
if (isThumbnail)
{
// Do not allow the logo to occupy more than ~30% of the thumbnail area
effectiveMaxLogoWidth = Math.Max(1, Math.Min(fotoLogoW, (int)(working.Width * 0.30)));
effectiveMaxLogoHeight = Math.Max(1, Math.Min(fotoLogoH, (int)(working.Height * 0.30)));
}
// Compute new logo size preserving aspect ratio, but avoid upscaling the logo beyond its native size
var targetByWidth = CalculateThumbnailSize(logoImg.Width, logoImg.Height, effectiveMaxLogoWidth, "Larghezza");
var targetByHeight = CalculateThumbnailSize(logoImg.Width, logoImg.Height, effectiveMaxLogoHeight, "Altezza");
var nuovaSize = (targetByWidth.Width <= targetByHeight.Width) ? targetByWidth : targetByHeight;
// Prevent upscaling: clamp to original logo size
if (nuovaSize.Width > logoImg.Width) nuovaSize.Width = logoImg.Width;
if (nuovaSize.Height > logoImg.Height) nuovaSize.Height = logoImg.Height;
// Parse logo margin (may be percentage string like "10%")
var logoMargineStr = _picSettings.LogoMargine ?? string.Empty;
var inPercentualeL = logoMargineStr.Trim().EndsWith('%');
var margineL = 0;
if (inPercentualeL)
{
var trimmed = logoMargineStr.Trim().TrimEnd('%');
if (!int.TryParse(trimmed, out margineL)) margineL = 0;
}
else
{
if (!int.TryParse(logoMargineStr, out margineL)) margineL = 0;
}
var margineUsato = inPercentualeL ? Convert.ToInt32(working.Height * margineL / (double)100) : margineL;
int xPosOfWm = 0;
int yPosOfWm = 0;
var logoH = (_picSettings.LogoPosizioneH ?? "NESSUNA").ToUpperInvariant();
var logoV = (_picSettings.LogoPosizioneV ?? "NESSUNA").ToUpperInvariant();
switch (logoH)
{
case "SINISTRA":
case "NESSUNA":
xPosOfWm = margineUsato;
break;
case "CENTRO":
xPosOfWm = Convert.ToInt32((working.Width - nuovaSize.Width) / (double)2);
break;
case "DESTRA":
xPosOfWm = ((working.Width - nuovaSize.Width) - margineUsato);
break;
}
switch (logoV)
{
case "ALTO":
case "NESSUNA":
yPosOfWm = margineUsato;
break;
case "CENTRO":
yPosOfWm = Convert.ToInt32((working.Height - nuovaSize.Height) / (double)2);
break;
case "BASSO":
yPosOfWm = ((working.Height - nuovaSize.Height) - margineUsato);
break;
}
var transparency = 1.0f;
if (int.TryParse(_picSettings.LogoTrasparenza, out var lt)) transparency = Math.Clamp(lt / 100f, 0f, 1f);
// Resize logo to nuovaSize
logoImg.Mutate(x => x.Resize(nuovaSize.Width, nuovaSize.Height));
// If configured to use color-key transparency for non-PNG logos, replace the key color with transparent
if (_picSettings.UseTransparentColor && !string.IsNullOrEmpty(_picSettings.TransparentColor))
{
try
{
var hex = _picSettings.TransparentColor.Trim();
// Allow either #RRGGBB or RRGGBB
if (hex.StartsWith("#")) hex = hex.Substring(1);
if (hex.Length == 6)
{
var r = Convert.ToByte(hex.Substring(0, 2), 16);
var g = Convert.ToByte(hex.Substring(2, 2), 16);
var b = Convert.ToByte(hex.Substring(4, 2), 16);
var key = SixLabors.ImageSharp.Color.FromRgb(r, g, b);
// Replace matching pixels (exact match) with transparent
logoImg.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
var row = accessor.GetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{
var p = row[x];
if (p.R == r && p.G == g && p.B == b)
{
row[x] = new Rgba32(p.R, p.G, p.B, 0);
}
}
}
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Invalid transparent color setting {Color}", _picSettings.TransparentColor);
}
}
// Ensure logo position keeps it inside the canvas (avoid clipping)
xPosOfWm = Math.Max(0, Math.Min(xPosOfWm, working.Width - nuovaSize.Width));
yPosOfWm = Math.Max(0, Math.Min(yPosOfWm, working.Height - nuovaSize.Height));
// Draw logo with opacity
working.Mutate(ctx => ctx.DrawImage(logoImg, new SixLabors.ImageSharp.Point(xPosOfWm, yPosOfWm), (float)transparency));
logoImg.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Error drawing logo in GDI pass");
_logger.LogError(ex, "[Alternate] Error drawing logo in ImageSharp pass");
}
}
@ -171,23 +391,14 @@ public class ImageCreatorAlternate : IImageCreator
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);
// 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);
}
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;
}
// Removed GDI encoder helper; ImageSharp encoders are used instead.
private void PrepareVariablesMinimal(ImageState imgState)
{
@ -196,6 +407,16 @@ public class ImageCreatorAlternate : IImageCreator
imgState.DimensioneStandard = _picSettings.DimStandard;
imgState.DimensioneStandardMiniatura = _picSettings.DimStandardMiniatura;
// basic text / transparency defaults used by drawing routines
// AlphaScelta mirrors ImageCreatorSharp behavior: compute from PicSettings.Trasparenza (0-100)
imgState.AlphaScelta = Convert.ToInt32((255 * (100 - _picSettings.Trasparenza) / (double)100));
// Set minimal text fields so text drawing has fallback values
imgState.TestoFirma ??= _picSettings.TestoFirmaStart ?? string.Empty;
imgState.TestoFirmaPiccola ??= string.Empty;
imgState.DataPartenzaI = _picSettings.DataPartenza;
imgState.TestoOrario = _picSettings.TestoOrario ?? string.Empty;
// sanitize
imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig);
imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall);
@ -297,5 +518,33 @@ public class ImageCreatorAlternate : IImageCreator
var newSize = new System.Drawing.Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier));
return newSize;
}
private static float FindBestFontSize(string text, string fontName, int maxSize, float maxWidth, float maxHeight, int minSize = 6)
{
if (maxSize <= minSize) return Math.Max(minSize, maxSize);
int low = minSize;
int high = Math.Max(minSize, maxSize);
int best = minSize;
while (low <= high)
{
int mid = (low + high) / 2;
// Approximate measurement: width ~ size * chars * 0.6, height ~ size
var approxWidth = mid * text.Length * 0.6f;
var approxHeight = mid * 1.0f;
if (approxWidth <= maxWidth && approxHeight <= maxHeight)
{
best = mid;
low = mid + 1; // try larger
}
else
{
high = mid - 1; // too big
}
}
return best;
}
}