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;