From 73597689ed361f77933f26231e0afd090ff28b6d Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Thu, 26 Feb 2026 19:17:23 +0100 Subject: [PATCH] Cross-platform: remove System.Drawing deps, add #if WINDOWS Refactored image creation APIs to use byte[] for logo data instead of System.Drawing.Image, enabling cross-platform support. Wrapped all GDI+/Windows-specific code in #if WINDOWS and updated project files to conditionally include Windows-only dependencies. Defaulted to ImageSharp on non-Windows, and updated UI and settings to reflect platform capabilities. Application now builds and runs on Linux/macOS with Avalonia and ImageSharp, while retaining full Windows functionality. --- MaddoShared/IImageCreator.cs | 3 +-- MaddoShared/ImageCreationStuff.cs | 26 ++++---------------- MaddoShared/ImageCreatorAlternate.cs | 16 +++++------- MaddoShared/ImageCreatorMapper.cs | 23 +++++++++++------- MaddoShared/ImageCreatorSharp.cs | 19 +++++++++------ MaddoShared/MaddoShared.csproj | 8 +++--- MaddoShared/PicSettings.cs | 1 - imagecatalog/AvaloniaMainWindow.axaml | 2 +- imagecatalog/DataModel.cs | 21 +++++++++++++++- imagecatalog/ImageCatalog 2.csproj | 21 ++++++++++------ imagecatalog/MainForm.Designer.cs | 6 +++-- imagecatalog/MainForm.cs | 6 +++-- imagecatalog/MainWindow.xaml.cs | 3 +++ imagecatalog/Models/SettingsDto.cs | 2 +- imagecatalog/Program.cs | 35 +++++++++++++-------------- imagecatalog/ViewModelBase.cs | 13 ++++++++-- 16 files changed, 115 insertions(+), 90 deletions(-) diff --git a/MaddoShared/IImageCreator.cs b/MaddoShared/IImageCreator.cs index 6fd85a5..4aa89c7 100644 --- a/MaddoShared/IImageCreator.cs +++ b/MaddoShared/IImageCreator.cs @@ -1,9 +1,8 @@ -using System.Drawing; using System.Threading.Tasks; namespace MaddoShared; public interface IImageCreator { - Task CreateImageAsync(ImageState imgState, Image logo); + Task CreateImageAsync(ImageState imgState, byte[]? logoData); } diff --git a/MaddoShared/ImageCreationStuff.cs b/MaddoShared/ImageCreationStuff.cs index 470e1d6..7d22457 100644 --- a/MaddoShared/ImageCreationStuff.cs +++ b/MaddoShared/ImageCreationStuff.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Drawing; using System.IO; using System.Linq; using System.Text; @@ -14,7 +13,6 @@ using Microsoft.Extensions.Logging; namespace MaddoShared { - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class ImageCreationService( ILogger logger, PicSettings picSettings, @@ -72,19 +70,15 @@ namespace MaddoShared // int threads = options.MaxThreads == 0 ? Environment.ProcessorCount * 2 : options.MaxThreads; int threads = options.MaxThreads; - Bitmap logoBmp = null; - // Load Logo (short-circuit) + // Load logo once as raw bytes (cross-platform). byte[] is safe to share across threads. + byte[]? logoBytes = null; if (picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile)) { - logoBmp = new Bitmap(picSettings.LogoNomeFile); + logoBytes = File.ReadAllBytes(picSettings.LogoNomeFile); } Func processFile = async fileData => { - Bitmap logoCopy = logoBmp is null - ? null - : logoBmp.Clone(new Rectangle(0, 0, logoBmp.Width, logoBmp.Height), logoBmp.PixelFormat); - var imgState = new ImageState { WorkFile = fileData.File, @@ -93,8 +87,7 @@ namespace MaddoShared try { - // Ensure CreateImageAsync can accept a null logoCopy value. - await imageCreatorService.CreateImageAsync(imgState, logoCopy); + await imageCreatorService.CreateImageAsync(imgState, logoBytes); results.Add(fileData.File.Name); @@ -110,8 +103,7 @@ namespace MaddoShared } finally { - // Dispose the clone if it was created - logoCopy?.Dispose(); + // nothing to dispose — byte[] is managed } }; @@ -139,14 +131,6 @@ namespace MaddoShared } } - try - { - logoBmp?.Dispose(); - } - catch (Exception e) - { - logger.LogError(e, "Error in disposing the logo"); - } } private List GetFilesToProcess(Options options) diff --git a/MaddoShared/ImageCreatorAlternate.cs b/MaddoShared/ImageCreatorAlternate.cs index d5af0f3..1a7d009 100644 --- a/MaddoShared/ImageCreatorAlternate.cs +++ b/MaddoShared/ImageCreatorAlternate.cs @@ -34,7 +34,7 @@ public class ImageCreatorImageSharp : IImageCreator _logger = logger; } - public async Task CreateImageAsync(ImageState imgState, System.Drawing.Image logo) + public async Task CreateImageAsync(ImageState imgState, byte[]? logoData) { ArgumentNullException.ThrowIfNull(imgState); @@ -74,7 +74,7 @@ public class ImageCreatorImageSharp : IImageCreator var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig); // Draw overlays (text/logo) onto big image using ImageSharp and save - await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logo, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false); + await DrawAndSaveWithGdiAsync(imgBig, fileNameBig, imgState, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false); // Create thumbnail if requested if (_picSettings.CreaMiniature) @@ -85,7 +85,7 @@ public class ImageCreatorImageSharp : IImageCreator var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall); // Draw overlays and save thumbnail via ImageSharp - await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logo, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false); + await DrawAndSaveWithGdiAsync(imgSmall, fileNameSmall, imgState, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false); } } catch (Exception ex) @@ -110,7 +110,7 @@ public class ImageCreatorImageSharp : IImageCreator }; } - private async Task DrawAndSaveWithGdiAsync(Image imgSharp, string outputPath, ImageState imgState, System.Drawing.Image logo, long quality, bool isThumbnail) + private async Task DrawAndSaveWithGdiAsync(Image imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail) { // Use ImageSharp drawing APIs to render text and logos and save using ImageSharp encoders. // Clone editable image so we don't mutate the original reference unexpectedly. @@ -287,13 +287,12 @@ public class ImageCreatorImageSharp : IImageCreator // Draw logo if provided. For compatibility with the original GDI implementation, // do not draw the logo on thumbnails (ImageCreatorSharp only draws logos on big images). - if (logo != null && _picSettings.LogoAggiungi && !isThumbnail) + if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail) { try { Image logoImg = null; - // 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); @@ -301,10 +300,7 @@ public class ImageCreatorImageSharp : IImageCreator } 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); + using var ms = new MemoryStream(logoData); logoImg = await SixLabors.ImageSharp.Image.LoadAsync(ms).ConfigureAwait(false); } diff --git a/MaddoShared/ImageCreatorMapper.cs b/MaddoShared/ImageCreatorMapper.cs index 32ee6ac..4772b7a 100644 --- a/MaddoShared/ImageCreatorMapper.cs +++ b/MaddoShared/ImageCreatorMapper.cs @@ -7,6 +7,7 @@ namespace MaddoShared; /// /// Dynamically resolves the concrete IImageCreator implementation at call time /// based on current PicSettings.ImageCreatorProvider. +/// On non-Windows platforms only ImageCreatorImageSharp is available. /// public class ImageCreatorMapper : IImageCreator { @@ -21,29 +22,33 @@ public class ImageCreatorMapper : IImageCreator _logger = logger; } - public Task CreateImageAsync(ImageState imgState, System.Drawing.Image logo) + public Task CreateImageAsync(ImageState imgState, byte[]? logoData) { var provider = (_settings.ImageCreatorProvider ?? "Sharp").Trim(); _logger?.LogDebug("Resolving IImageCreator for provider '{Provider}'", provider); +#if WINDOWS return provider.Equals("ALTERNATE", StringComparison.OrdinalIgnoreCase) - ? ResolveAndCall(imgState, logo) - : ResolveAndCall(imgState, logo); + ? ResolveAndCall(imgState, logoData) + : ResolveAndCall(imgState, logoData); +#else + // GDI is not available on non-Windows — always use ImageSharp + return ResolveAndCall(imgState, logoData); +#endif } - private Task ResolveAndCall(ImageState imgState, System.Drawing.Image logo) where T : IImageCreator + private Task ResolveAndCall(ImageState imgState, byte[]? logoData) where T : IImageCreator { - // Resolve the concrete implementation and forward the call - var impl = (IImageCreator)_sp.GetService(typeof(T)); + var impl = (IImageCreator?)_sp.GetService(typeof(T)); if (impl is null) { - _logger?.LogWarning("Requested image creator {Type} is not registered. Falling back to ImageCreatorGDI.", typeof(T).Name); - impl = (IImageCreator)_sp.GetService(typeof(ImageCreatorGDI)); + _logger?.LogWarning("Requested image creator {Type} is not registered. Falling back to ImageCreatorImageSharp.", typeof(T).Name); + impl = (IImageCreator?)_sp.GetService(typeof(ImageCreatorImageSharp)); } if (impl is null) throw new InvalidOperationException("No IImageCreator implementation is registered."); - return impl.CreateImageAsync(imgState, logo); + return impl.CreateImageAsync(imgState, logoData); } } diff --git a/MaddoShared/ImageCreatorSharp.cs b/MaddoShared/ImageCreatorSharp.cs index fdac999..0b28d9f 100644 --- a/MaddoShared/ImageCreatorSharp.cs +++ b/MaddoShared/ImageCreatorSharp.cs @@ -1,4 +1,5 @@ -using System; +#if WINDOWS +using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; @@ -17,7 +18,7 @@ namespace MaddoShared; [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class ImageCreatorGDI(PicSettings picSettings, ILogger logger) : IImageCreator { - public async Task CreateImageAsync(ImageState imgState, Image logo) + public async Task CreateImageAsync(ImageState imgState, byte[]? logoData) { try { @@ -51,7 +52,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l AddText(g, imgState, imgOutputBig); - AddLogo(imgOutputBig, logo); + AddLogo(imgOutputBig, logoData); SavePhoto(imgOutputBig, imgState, thisFormat); }).ConfigureAwait(false); @@ -480,13 +481,14 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l } } - private void AddLogo(Bitmap imgOutputBig, Image logo) + private void AddLogo(Bitmap imgOutputBig, byte[]? logoData) { - // Skip if no logo provided - if (logo is null) return; + // Skip if no logo bytes provided + if (logoData is null) return; - // Load check (use short-circuit &&) - if (!(picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile))) return; + if (!picSettings.LogoAggiungi) return; + + using var logo = Image.FromStream(new System.IO.MemoryStream(logoData)); // Decide whether to apply a color-key transparency remap or rely on existing image alpha. // If UseTransparentColor is true, parse the configured TransparentColor and remap it to fully transparent. @@ -861,3 +863,4 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l : Environment.NewLine + formatted; } } +#endif // WINDOWS diff --git a/MaddoShared/MaddoShared.csproj b/MaddoShared/MaddoShared.csproj index 2bd6b04..b18b80a 100644 --- a/MaddoShared/MaddoShared.csproj +++ b/MaddoShared/MaddoShared.csproj @@ -1,11 +1,11 @@  - net10.0-windows + net10.0 Library false - true - true x64 + + $(DefineConstants);WINDOWS @@ -29,6 +29,6 @@ all - + \ No newline at end of file diff --git a/MaddoShared/PicSettings.cs b/MaddoShared/PicSettings.cs index 6729bf6..b633db3 100644 --- a/MaddoShared/PicSettings.cs +++ b/MaddoShared/PicSettings.cs @@ -1,7 +1,6 @@ using System; using System.Drawing; using System.IO; -using System.Windows.Forms; namespace MaddoShared; diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index a16b949..e47c50d 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -73,7 +73,7 @@ - + diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index ca7c44a..c7b0300 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -5,12 +5,17 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +#if WINDOWS using System.Drawing.Text; +#endif using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; +#if WINDOWS using System.Windows.Forms; +#endif using System.Windows.Input; using AutoMapper; using MaddoShared; @@ -258,12 +263,16 @@ namespace ImageCatalog_2 private List LoadAvailableFonts() { +#if WINDOWS var fonts = new List(); using (var installedFonts = new InstalledFontCollection()) { fonts.AddRange(installedFonts.Families.Select(f => f.Name)); } return fonts; +#else + return new List(); +#endif } private CancellationTokenSource? _mainToken; @@ -604,7 +613,12 @@ namespace ImageCatalog_2 } // Image library selection (UI radio buttons bind to the boolean helpers) - private string _imageLibrary = "System.Graphics"; + private string _imageLibrary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "System.Graphics" : "ImageSharp"; + + /// + /// Whether the application is running on Windows. Used by cross-platform UIs to show/hide Windows-only options. + /// + public bool IsRunningOnWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// The selected image processing library. Possible values: "System.Graphics" or "ImageSharp". @@ -624,6 +638,7 @@ namespace ImageCatalog_2 NotifyPropertyChanged(); NotifyPropertyChanged(nameof(UseSystemGraphics)); NotifyPropertyChanged(nameof(UseImageSharp)); + NotifyPropertyChanged(nameof(IsRunningOnWindows)); } } @@ -1506,7 +1521,11 @@ namespace ImageCatalog_2 public event EventHandler LoadSettingsRequested; public event EventHandler SelectColorRequested; // Request that the View shows a message to the user (message, caption, icon) +#if WINDOWS public event EventHandler> ShowMessageRequested; +#else + public event EventHandler> ShowMessageRequested; +#endif public event EventHandler SelectTransparentColorRequested; private void SelectSourceFolder(object parameter) diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index d5724d5..1654ae5 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -1,19 +1,24 @@  - WinExe - net10.0-windows enable enable - true - true False ImageCatalog default + + + + net10.0-windows + WinExe + true + true + win-x64 Logo.ico - - win-x64 + + net10.0 + Exe