develop #1

Open
maddo wants to merge 126 commits from develop into master
7 changed files with 474 additions and 62 deletions
Showing only changes of commit 63751af18d - Show all commits

Add selectable image library option and refactor processing

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

View file

@ -10,8 +10,8 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
</ItemGroup>
<ItemGroup>

View file

@ -17,8 +17,8 @@
<ItemGroup>
<PackageReference Include="Moq" Version="4.20.2" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
</ItemGroup>
<ItemGroup>

View file

@ -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.
/// <summary>
/// Image creator implemented using SixLabors.ImageSharp for core image operations.
/// This implementation focuses on loading, EXIF-orientation, resizing and saving.
/// It intentionally implements a minimal subset of the original functionality to
/// provide a safe and testable replacement. Additional features (text/logo drawing)
/// can be added later using ImageSharp.Drawing.Common and SixLabors.Fonts.
/// </summary>
public class ImageCreatorAlternate : IImageCreator
{
private readonly ImageCreatorSharp _inner;
private readonly PicSettings _picSettings;
private readonly ILogger<ImageCreatorAlternate> _logger;
public ImageCreatorAlternate(ImageCreatorSharp inner, ILogger<ImageCreatorAlternate> logger)
public ImageCreatorAlternate(PicSettings picSettings, ILogger<ImageCreatorAlternate> 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<Rgba32>(fs).ConfigureAwait(false);
// Extract EXIF info (orientation and creation date)
ExtractExif(img, imgState);
// Apply orientation
ApplyExifOrientation(img, imgState);
// Determine output format
var forceJpg = _picSettings.UsaForzaJpg;
// Compute big size
var bigSize = ComputeBigSize(img.Width, img.Height);
// Resize big image if needed
using var imgBig = img.Clone(ctx => ctx.Resize(bigSize.Width, bigSize.Height));
// Ensure destination exists
imgState.DestDir?.Create();
var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig);
// Draw overlays (text/logo) onto big image via GDI+ and save
await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logo, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false);
// Save big image with quality if JPEG
var extBig = System.IO.Path.GetExtension(imgState.NomeFileBig)?.ToLowerInvariant() ?? string.Empty;
var encoderBig = GetEncoderForExtension(extBig, _picSettings.JpegQuality);
await using (var outStream = System.IO.File.Open(fileNameBig, System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
await imgBig.SaveAsync(outStream, encoderBig, default).ConfigureAwait(false);
}
// Create thumbnail if requested
if (_picSettings.CreaMiniature)
{
var smallSize = ComputeSmallSize(img.Width, img.Height);
using var imgSmall = img.Clone(ctx => ctx.Resize(smallSize.Width, smallSize.Height));
var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall);
// Draw overlays and save thumbnail via GDI+
await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logo, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Error processing image {File}", imgState.WorkFile?.Name);
throw;
}
}
// Thumbnail overlays are rendered by the GDI+ pass in DrawAndSaveWithGdiAsync to match original rendering.
private static SixLabors.ImageSharp.Formats.IImageEncoder GetEncoderForExtension(string ext, long quality)
{
quality = Math.Clamp(quality, 1, 100);
return ext switch
{
".png" => new SixLabors.ImageSharp.Formats.Png.PngEncoder(),
".gif" => new SixLabors.ImageSharp.Formats.Gif.GifEncoder(),
_ => new JpegEncoder { Quality = (int)quality },
};
}
private async Task DrawAndSaveWithGdiAsync(Image<Rgba32> imgSharp, string outputPath, ImageState imgState, System.Drawing.Image logo, long quality, bool isThumbnail)
{
// Convert ImageSharp image to System.Drawing.Bitmap via MemoryStream PNG to preserve alpha
await using var ms = new MemoryStream();
await imgSharp.SaveAsPngAsync(ms).ConfigureAwait(false);
ms.Seek(0, SeekOrigin.Begin);
using var bmp = new Bitmap(ms);
using var g = Graphics.FromImage(bmp);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// Prepare text
var text = isThumbnail ? imgState.TestoFirmaPiccola : imgState.TestoFirma;
if (string.IsNullOrEmpty(text) && _picSettings.TestoNome)
text = imgState.NomeFileBig;
if (!string.IsNullOrEmpty(text))
{
var fontSize = isThumbnail ? imgState.DimensioneStandardMiniatura : imgState.DimensioneStandard;
using var font = new System.Drawing.Font(_picSettings.IlFont ?? "Arial", Math.Max(6, fontSize));
using var shadowBrush = new SolidBrush(System.Drawing.Color.FromArgb(imgState.AlphaScelta, 0, 0, 0));
using var textBrush = new SolidBrush(System.Drawing.Color.FromArgb(imgState.AlphaScelta, _picSettings.FontColoreRGB));
var sf = new StringFormat { Alignment = StringAlignment.Center };
var x = bmp.Width / 2f;
var y = isThumbnail ? bmp.Height - fontSize - 4 : bmp.Height - fontSize - (_picSettings.Margine);
g.DrawString(text, font, shadowBrush, new System.Drawing.PointF(x + 1, y + 1), sf);
g.DrawString(text, font, textBrush, new System.Drawing.PointF(x, y), sf);
}
// Draw logo if provided
if (logo != null && _picSettings.LogoAggiungi && File.Exists(_picSettings.LogoNomeFile))
{
try
{
var target = new System.Drawing.Size(_picSettings.LogoLarghezza, _picSettings.LogoAltezza);
using var logoResized = new Bitmap(logo, target.Width, target.Height);
int xPos = _picSettings.LogoPosizioneH?.ToUpperInvariant() == "DESTRA" ? bmp.Width - target.Width - _picSettings.Margine : _picSettings.Margine;
int yPos = _picSettings.LogoPosizioneV?.ToUpperInvariant() == "BASSO" ? bmp.Height - target.Height - _picSettings.Margine : _picSettings.Margine;
var cm = new System.Drawing.Imaging.ColorMatrix { Matrix33 = (float)Math.Clamp((int.TryParse(_picSettings.LogoTrasparenza, out var lt) ? lt : 100) / 100.0, 0.0, 1.0) };
var ia = new System.Drawing.Imaging.ImageAttributes();
ia.SetColorMatrix(cm, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap);
g.DrawImage(logoResized, new System.Drawing.Rectangle(xPos, yPos, target.Width, target.Height), 0, 0, target.Width, target.Height, GraphicsUnit.Pixel, ia);
}
catch (Exception ex)
{
_logger.LogError(ex, "[Alternate] Error drawing logo in GDI pass");
}
}
// Ensure directory
var dir = System.IO.Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
// Save with requested quality using GDI encoder
var encoder = GetEncoder(ImageFormat.Jpeg);
var myEncoder = System.Drawing.Imaging.Encoder.Quality;
using var encoderParams = new System.Drawing.Imaging.EncoderParameters(1);
encoderParams.Param[0] = new System.Drawing.Imaging.EncoderParameter(myEncoder, quality);
bmp.Save(outputPath, encoder, encoderParams);
}
private static ImageCodecInfo GetEncoder(System.Drawing.Imaging.ImageFormat format)
{
var codecs = ImageCodecInfo.GetImageDecoders();
foreach (var codec in codecs)
{
if (codec.FormatID == format.Guid) return codec;
}
return null;
}
private void PrepareVariablesMinimal(ImageState imgState)
{
imgState.NomeFileBig = imgState.WorkFile.Name;
imgState.NomeFileSmall = (_picSettings.Suffisso ?? string.Empty) + imgState.WorkFile.Name;
imgState.DimensioneStandard = _picSettings.DimStandard;
imgState.DimensioneStandardMiniatura = _picSettings.DimStandardMiniatura;
// sanitize
imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig);
imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall);
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return fileName;
var invalid = System.IO.Path.GetInvalidFileNameChars();
var sb = new System.Text.StringBuilder(fileName.Length);
foreach (var ch in fileName)
sb.Append(Array.IndexOf(invalid, ch) >= 0 ? '_' : ch);
return sb.ToString();
}
private void ExtractExif(Image<Rgba32> img, ImageState imgState)
{
imgState.Orientation = Orientations.TopLeft;
imgState.CreationDate = null;
var profile = img.Metadata?.ExifProfile;
if (profile is null) return;
IExifValue<ushort> rotation = null;
var found = profile.TryGetValue(ExifTag.Orientation, out rotation);
if (found && rotation != null)
{
imgState.Orientation = (Orientations)Convert.ToInt32(rotation.Value);
}
IExifValue<string> date = null;
var creationFound = profile.TryGetValue(ExifTag.DateTimeOriginal, out date);
if (creationFound && date != null)
{
if (DateTime.TryParseExact(date.Value, "yyyy:MM:dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var cr))
{
imgState.CreationDate = cr;
}
else
{
imgState.CreationDate = null;
}
}
else
{
imgState.CreationDate = null;
}
}
private void ApplyExifOrientation(Image<Rgba32> img, ImageState imgState)
{
// Common EXIF orientations: 1=TopLeft, 3=BottomRight (rotate 180), 6=RightTop (rotate 90 CW), 8=LeftBottom (rotate 270 CW)
switch (imgState.Orientation)
{
case Orientations.RightTop: // 6
img.Mutate(x => x.Rotate(RotateMode.Rotate90));
break;
case Orientations.BottomRight: // 3
img.Mutate(x => x.Rotate(RotateMode.Rotate180));
break;
case Orientations.LftBottom: // 8
img.Mutate(x => x.Rotate(RotateMode.Rotate270));
break;
default:
break;
}
}
private System.Drawing.Size ComputeBigSize(int width, int height)
{
// If original large size option requested, return original
// otherwise compute based on width/height limits from settings
return width > height
? CalculateThumbnailSize(width, height, _picSettings.LarghezzaBig, "Larghezza")
: CalculateThumbnailSize(width, height, _picSettings.AltezzaBig, "Altezza");
}
private System.Drawing.Size ComputeSmallSize(int width, int height)
{
return width > height
? CalculateThumbnailSize(width, height, _picSettings.LarghezzaSmall, "Larghezza")
: CalculateThumbnailSize(width, height, _picSettings.AltezzaSmall, "Altezza");
}
// Helper to access PicSettings values via instance _picSettings
private static System.Drawing.Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize)
{
double tempMultiplier;
if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase))
tempMultiplier = maxPixel / (double)currentwidth;
else if (string.Equals(tipoSize, "Altezza", StringComparison.OrdinalIgnoreCase))
tempMultiplier = maxPixel / (double)currentheight;
else if (currentheight > currentwidth)
tempMultiplier = maxPixel / (double)currentheight;
else
tempMultiplier = maxPixel / (double)currentwidth;
var newSize = new System.Drawing.Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier));
return newSize;
}
}

View file

@ -14,12 +14,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="System.Buffers" Version="4.6.1" />
<PackageReference Include="System.Collections.Immutable" Version="9.0.7" />
<PackageReference Include="System.Collections.Immutable" Version="10.0.3" />
<PackageReference Include="System.Memory" Version="4.6.3" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
@ -27,6 +29,6 @@
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="9.0.7" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.3" />
</ItemGroup>
</Project>

View file

@ -397,6 +397,50 @@ namespace ImageCatalog_2
}
}
// Image library selection (UI radio buttons bind to the boolean helpers)
private string _imageLibrary = "System.Graphics";
/// <summary>
/// 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.
/// </summary>
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

View file

@ -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
{

View file

@ -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<MainForm> 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;