diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj
index 99dc727..7ce2437 100644
--- a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj
+++ b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj
@@ -10,8 +10,8 @@
-
-
+
+
diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj
index 0b2620b..c4a7299 100644
--- a/MaddoShared.Tests/MaddoShared.Tests.csproj
+++ b/MaddoShared.Tests/MaddoShared.Tests.csproj
@@ -17,8 +17,8 @@
-
-
+
+
diff --git a/MaddoShared/ImageCreatorAlternate.cs b/MaddoShared/ImageCreatorAlternate.cs
index 2c945e8..9ff74ec 100644
--- a/MaddoShared/ImageCreatorAlternate.cs
+++ b/MaddoShared/ImageCreatorAlternate.cs
@@ -1,25 +1,301 @@
-using System.Drawing;
+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;
-// Minimal alternate adapter that currently delegates to ImageCreatorSharp.
-// Later this can be replaced with a different library implementation.
+///
+/// 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.
+///
public class ImageCreatorAlternate : IImageCreator
{
- private readonly ImageCreatorSharp _inner;
+ private readonly PicSettings _picSettings;
private readonly ILogger _logger;
- public ImageCreatorAlternate(ImageCreatorSharp inner, ILogger logger)
+ public ImageCreatorAlternate(PicSettings picSettings, ILogger logger)
{
- _inner = inner;
+ _picSettings = picSettings ?? throw new ArgumentNullException(nameof(picSettings));
_logger = logger;
}
- public Task CreateImageAsync(ImageState imgState, Image logo)
+ public async Task CreateImageAsync(ImageState imgState, System.Drawing.Image logo)
{
- _logger.LogDebug("Using alternate image creator adapter");
- return _inner.CreateImageAsync(imgState, 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(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 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 img, ImageState imgState)
+ {
+ imgState.Orientation = Orientations.TopLeft;
+ imgState.CreationDate = null;
+
+ var profile = img.Metadata?.ExifProfile;
+ if (profile is null) return;
+
+ IExifValue rotation = null;
+ var found = profile.TryGetValue(ExifTag.Orientation, out rotation);
+ if (found && rotation != null)
+ {
+ imgState.Orientation = (Orientations)Convert.ToInt32(rotation.Value);
+ }
+
+ IExifValue 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 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;
}
}
+
diff --git a/MaddoShared/MaddoShared.csproj b/MaddoShared/MaddoShared.csproj
index e95dcf4..2bd6b04 100644
--- a/MaddoShared/MaddoShared.csproj
+++ b/MaddoShared/MaddoShared.csproj
@@ -14,12 +14,14 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
+
+
-
+
@@ -27,6 +29,6 @@
all
-
+
\ No newline at end of file
diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs
index 02c7b76..d59cb07 100644
--- a/imagecatalog/DataModel.cs
+++ b/imagecatalog/DataModel.cs
@@ -397,6 +397,50 @@ namespace ImageCatalog_2
}
}
+ // Image library selection (UI radio buttons bind to the boolean helpers)
+ private string _imageLibrary = "System.Graphics";
+
+ ///
+ /// The selected image processing library. Possible values: "System.Graphics" or "ImageSharp".
+ /// This value is mirrored into PicSettings.ImageCreatorProvider so the runtime mapper picks the implementation.
+ ///
+ public string ImageLibrary
+ {
+ get => _imageLibrary;
+ set
+ {
+ if (_imageLibrary == value) return;
+ _imageLibrary = value;
+ // Reflect selection into PicSettings so mapper can resolve at runtime
+ _picSettings.ImageCreatorProvider = string.Equals(value, "ImageSharp", StringComparison.OrdinalIgnoreCase)
+ ? "ALTERNATE"
+ : "Sharp";
+ NotifyPropertyChanged();
+ NotifyPropertyChanged(nameof(UseSystemGraphics));
+ NotifyPropertyChanged(nameof(UseImageSharp));
+ }
+ }
+
+ public bool UseSystemGraphics
+ {
+ get => string.Equals(ImageLibrary, "System.Graphics", StringComparison.OrdinalIgnoreCase);
+ set
+ {
+ if (value) ImageLibrary = "System.Graphics";
+ NotifyPropertyChanged();
+ }
+ }
+
+ public bool UseImageSharp
+ {
+ get => string.Equals(ImageLibrary, "ImageSharp", StringComparison.OrdinalIgnoreCase);
+ set
+ {
+ if (value) ImageLibrary = "ImageSharp";
+ NotifyPropertyChanged();
+ }
+ }
+
// Folder division settings
private int _filesPerFolder = 99;
public int FilesPerFolder
diff --git a/imagecatalog/MainForm.Designer.cs b/imagecatalog/MainForm.Designer.cs
index ed17967..5865046 100644
--- a/imagecatalog/MainForm.Designer.cs
+++ b/imagecatalog/MainForm.Designer.cs
@@ -45,8 +45,6 @@ namespace ImageCatalog
Label43 = new Label();
TabControl1 = new TabControl();
TabPage5 = new TabPage();
- button1 = new Button();
- btnTest = new Button();
GroupBox11 = new GroupBox();
numericUpDown2 = new NumericUpDown();
numericUpDown1 = new NumericUpDown();
@@ -183,6 +181,9 @@ namespace ImageCatalog
_btnCreaCatalogoAsync = new Button();
timer1 = new System.Windows.Forms.Timer(components);
dataModelBindingSource1 = new BindingSource(components);
+ groupBox12 = new GroupBox();
+ rdbLibrary1 = new RadioButton();
+ rdbLibrary2 = new RadioButton();
((System.ComponentModel.ISupportInitialize)bindingSource1).BeginInit();
((System.ComponentModel.ISupportInitialize)dataModelBindingSource).BeginInit();
TabControl1.SuspendLayout();
@@ -211,6 +212,7 @@ namespace ImageCatalog
((System.ComponentModel.ISupportInitialize)_PictureBox1).BeginInit();
((System.ComponentModel.ISupportInitialize)PictureBox3).BeginInit();
((System.ComponentModel.ISupportInitialize)dataModelBindingSource1).BeginInit();
+ groupBox12.SuspendLayout();
SuspendLayout();
//
// ProgressBar1
@@ -270,8 +272,7 @@ namespace ImageCatalog
//
// TabPage5
//
- TabPage5.Controls.Add(button1);
- TabPage5.Controls.Add(btnTest);
+ TabPage5.Controls.Add(groupBox12);
TabPage5.Controls.Add(GroupBox11);
TabPage5.Controls.Add(GroupBox3);
TabPage5.Controls.Add(GroupBox8);
@@ -285,28 +286,6 @@ namespace ImageCatalog
TabPage5.Text = "Generale";
TabPage5.UseVisualStyleBackColor = true;
//
- // button1
- //
- button1.DataBindings.Add(new Binding("Command", bindingSource1, "AsyncTestCommand", true));
- button1.Location = new Point(751, 720);
- button1.Margin = new Padding(5);
- button1.Name = "button1";
- button1.Size = new Size(141, 43);
- button1.TabIndex = 50;
- button1.Text = "Test Async";
- button1.UseVisualStyleBackColor = true;
- //
- // btnTest
- //
- btnTest.DataBindings.Add(new Binding("Command", bindingSource1, "TestCommand", true));
- btnTest.Location = new Point(487, 708);
- btnTest.Margin = new Padding(5);
- btnTest.Name = "btnTest";
- btnTest.Size = new Size(141, 43);
- btnTest.TabIndex = 49;
- btnTest.Text = "Test";
- btnTest.UseVisualStyleBackColor = true;
- //
// GroupBox11
//
GroupBox11.Controls.Add(numericUpDown2);
@@ -1750,6 +1729,7 @@ namespace ImageCatalog
//
// versionLabel
//
+ versionLabel.DataBindings.Add(new Binding("Text", bindingSource1, "AppVersion", true));
versionLabel.Location = new Point(1182, 873);
versionLabel.Margin = new Padding(6, 0, 6, 0);
versionLabel.Name = "versionLabel";
@@ -1757,7 +1737,6 @@ namespace ImageCatalog
versionLabel.TabIndex = 62;
versionLabel.Text = "Versione 2.2 2021";
versionLabel.TextAlign = ContentAlignment.MiddleRight;
- versionLabel.DataBindings.Add(new Binding("Text", bindingSource1, "AppVersion", true));
//
// _Button7
//
@@ -1863,6 +1842,41 @@ namespace ImageCatalog
//
dataModelBindingSource1.DataSource = typeof(ImageCatalog_2.DataModel);
//
+ // groupBox12
+ //
+ groupBox12.Controls.Add(rdbLibrary2);
+ groupBox12.Controls.Add(rdbLibrary1);
+ groupBox12.Location = new Point(405, 625);
+ groupBox12.Name = "groupBox12";
+ groupBox12.Size = new Size(350, 175);
+ groupBox12.TabIndex = 49;
+ groupBox12.TabStop = false;
+ groupBox12.Text = "Libreria Manipolazione Grafica";
+ //
+ // rdbLibrary1
+ //
+ rdbLibrary1.AutoSize = true;
+ rdbLibrary1.Location = new Point(12, 37);
+ rdbLibrary1.Name = "rdbLibrary1";
+ rdbLibrary1.Size = new Size(188, 34);
+ rdbLibrary1.TabIndex = 0;
+ rdbLibrary1.TabStop = true;
+ rdbLibrary1.Text = "System.Graphics";
+ rdbLibrary1.DataBindings.Add(new Binding("Checked", bindingSource1, "UseSystemGraphics", true, DataSourceUpdateMode.OnPropertyChanged));
+ rdbLibrary1.UseVisualStyleBackColor = true;
+ //
+ // rdbLibrary2
+ //
+ rdbLibrary2.AutoSize = true;
+ rdbLibrary2.Location = new Point(12, 77);
+ rdbLibrary2.Name = "rdbLibrary2";
+ rdbLibrary2.Size = new Size(149, 34);
+ rdbLibrary2.TabIndex = 1;
+ rdbLibrary2.TabStop = true;
+ rdbLibrary2.Text = "ImageSharp";
+ rdbLibrary2.DataBindings.Add(new Binding("Checked", bindingSource1, "UseImageSharp", true, DataSourceUpdateMode.OnPropertyChanged));
+ rdbLibrary2.UseVisualStyleBackColor = true;
+ //
// MainForm
//
AutoScaleDimensions = new SizeF(12F, 30F);
@@ -1929,6 +1943,8 @@ namespace ImageCatalog
((System.ComponentModel.ISupportInitialize)_PictureBox1).EndInit();
((System.ComponentModel.ISupportInitialize)PictureBox3).EndInit();
((System.ComponentModel.ISupportInitialize)dataModelBindingSource1).EndInit();
+ groupBox12.ResumeLayout(false);
+ groupBox12.PerformLayout();
ResumeLayout(false);
PerformLayout();
}
@@ -2275,12 +2291,13 @@ namespace ImageCatalog
private BindingSource dataModelBindingSource;
private BindingSource dataModelBindingSource1;
private BindingSource bindingSource1;
- private Button btnTest;
- private Button button1;
private NumericUpDown numericUpDown1;
private NumericUpDown numericUpDown2;
private Button btnOpenDestFolder;
private Button btnOpenSourceFolder;
+ private GroupBox groupBox12;
+ private RadioButton rdbLibrary2;
+ private RadioButton rdbLibrary1;
internal Button btnCreaCatalogoAsync
{
diff --git a/imagecatalog/MainForm.cs b/imagecatalog/MainForm.cs
index d63af72..79574a4 100644
--- a/imagecatalog/MainForm.cs
+++ b/imagecatalog/MainForm.cs
@@ -18,7 +18,6 @@ using ImageCatalog_2.Commands;
using ImageCatalog_2.Services;
using MaddoShared;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging;
namespace ImageCatalog;
@@ -30,6 +29,8 @@ public partial class MainForm
private readonly ParametriSetup _parametriSetup;
private readonly PicSettings _picSettings;
+ // Prevent re-entrant updates between UI events and model PropertyChanged handling
+ private bool _suppressRadioUpdates = false;
public MainForm(DataModel model, ImageCreationStuff imageCreationStuff, PicSettings picSettings,
ParametriSetup parametriSetup, ILogger logger)
@@ -42,11 +43,44 @@ public partial class MainForm
_logger.LogDebug("Start");
InitializeComponent();
-
- // Set this form as the control for thread marshalling in the DataModel
- Model.SetControl(this);
-
- BindControls();
+ // Set this form as the control for thread marshalling in the DataModel
+ Model.SetControl(this);
+
+ // Ensure the designer data bindings have a concrete DataSource immediately so
+ // that UI controls (radio buttons) reflect the ViewModel state and propagate
+ // user changes back to the ViewModel.
+ bindingSource1.DataSource = Model;
+
+ BindControls();
+
+ // The designer originally bound the radio buttons to boolean helpers on the ViewModel.
+ // Those two separate bindings can fight with each other. Clear designer bindings and
+ // wire explicit Click handlers that update the single authoritative property
+ // `Model.ImageLibrary`. Also keep a PropertyChanged listener to reflect external
+ // changes back into the radio buttons.
+ rdbLibrary1.DataBindings.Clear();
+ rdbLibrary2.DataBindings.Clear();
+
+ // Initialize radio state from model
+ rdbLibrary1.Checked = Model.UseSystemGraphics;
+ rdbLibrary2.Checked = Model.UseImageSharp;
+
+ // Use Click handlers (not CheckedChanged) to avoid competing binding updates
+ rdbLibrary1.Click += (_, _) =>
+ {
+ if (_suppressRadioUpdates) return;
+ if (Model.ImageLibrary != "System.Graphics")
+ Model.ImageLibrary = "System.Graphics";
+ };
+ rdbLibrary2.Click += (_, _) =>
+ {
+ if (_suppressRadioUpdates) return;
+ if (Model.ImageLibrary != "ImageSharp")
+ Model.ImageLibrary = "ImageSharp";
+ };
+
+ // Watch for model changes so we can reflect external updates
+ Model.PropertyChanged += Model_PropertyChanged;
// Save user preferences on form close instead of immediately when dialogs are used
this.FormClosing += MainForm_FormClosing;
@@ -58,17 +92,56 @@ public partial class MainForm
// Version label is data-bound to DataModel.AppVersion; DataModel is populated with the version via DI
}
- protected void BindControls()
+ private void RdbLibrary_CheckedChanged(object? sender, EventArgs e)
{
- // Bind buttons to ViewModel commands using command binding
- _btnCreaCatalogoAsync.BindCommand(Model.ProcessImagesCommand);
- button1.BindCommand(Model.ProcessImagesCommand);
- _Button2.BindCommand(Model.SelectSourceFolderCommand);
- _Button3.BindCommand(Model.SelectDestinationFolderCommand);
- _Button4.BindCommand(Model.SelectLogoFileCommand);
- _Button5.BindCommand(Model.SaveSettingsCommand);
- _Button6.BindCommand(Model.LoadSettingsCommand);
- _Button8.BindCommand(Model.SelectColorCommand);
+ // Keep behavior simple: when a radio button becomes checked, update the ViewModel
+ // so that the designer binding and PicSettings stay in sync.
+ if (sender is RadioButton rb && rb.Checked)
+ {
+ _logger?.LogDebug("Radio library changed: {RadioName}", rb.Name);
+ if (rb == rdbLibrary2)
+ {
+ Model.ImageLibrary = "ImageSharp";
+ }
+ else if (rb == rdbLibrary1)
+ {
+ Model.ImageLibrary = "System.Graphics";
+ }
+ }
+ }
+
+ private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is null) return;
+ if (e.PropertyName == nameof(Model.ImageLibrary) || e.PropertyName == nameof(Model.UseImageSharp) || e.PropertyName == nameof(Model.UseSystemGraphics))
+ {
+ _logger?.LogDebug("Model property changed: {Property} => ImageLibrary={ImageLibrary}, PicSettings.Provider={Provider}", e.PropertyName, Model.ImageLibrary, _picSettings.ImageCreatorProvider);
+
+ // Reflect authoritative model value into the radio buttons in a thread-safe, re-entrancy-safe way
+ try
+ {
+ _suppressRadioUpdates = true;
+ rdbLibrary1.Checked = Model.UseSystemGraphics;
+ rdbLibrary2.Checked = Model.UseImageSharp;
+ }
+ finally
+ {
+ _suppressRadioUpdates = false;
+ }
+ }
+ }
+
+ protected void BindControls()
+ {
+ // Bind buttons to ViewModel commands using command binding
+ _btnCreaCatalogoAsync.BindCommand(Model.ProcessImagesCommand);
+ // Note: `button1` control does not exist in the designer. Use the primary create button only.
+ _Button2.BindCommand(Model.SelectSourceFolderCommand);
+ _Button3.BindCommand(Model.SelectDestinationFolderCommand);
+ _Button4.BindCommand(Model.SelectLogoFileCommand);
+ _Button5.BindCommand(Model.SaveSettingsCommand);
+ _Button6.BindCommand(Model.LoadSettingsCommand);
+ _Button8.BindCommand(Model.SelectColorCommand);
// Subscribe to ViewModel events for UI dialogs (these need UI context)
Model.SelectSourceFolderRequested += OnSelectSourceFolderRequested;