From d76e133f188088c2f2d28fc9801e0a3586a40d76 Mon Sep 17 00:00:00 2001 From: Maddo Date: Thu, 28 May 2026 20:27:05 +0200 Subject: [PATCH] Completely removed GDI --- .forgejo/workflows/build-windows-avalonia.yml | 7 + .../workflows/release-windows-avalonia.yml | 7 + .github/copilot-instructions.md | 10 +- .gitlab-ci.yml | 7 + CatalogLite/CatalogConfigurationLoader.cs | 25 +- CatalogLite/CatalogLite.csproj | 3 +- CatalogLite/LiteCatalogViewModel.cs | 1 - MaddoShared.Benchmarks/ChunkSizeBenchmarks.cs | 4 +- .../Helpers/TestImageGenerator.cs | 105 +- .../ImageProcessingBenchmarks.cs | 6 +- MaddoShared.Benchmarks/ImageSizeBenchmarks.cs | 4 +- .../MaddoShared.Benchmarks.csproj | 4 +- MaddoShared.Benchmarks/StressTestBenchmark.cs | 18 +- .../MaddoShared.ImageSharpTests.csproj | 6 +- MaddoShared.Tests/ImageCreatorSharpTests.cs | 278 +--- MaddoShared.Tests/MaddoShared.Tests.csproj | 1 - MaddoShared/ImageCreatorGDI.cs | 866 ------------ MaddoShared/ImageCreatorImageSharp.cs | 58 +- MaddoShared/ImageCreatorMapper.cs | 54 - MaddoShared/ImageState.cs | 23 +- MaddoShared/MaddoShared.csproj | 7 +- MaddoShared/PicSettings.cs | 6 +- README.md | 2 +- docs/image-generation-tests-plan.md | 2 +- .../AvaloniaViews/GeneralTabView.axaml | 6 - imagecatalog/DataModel.cs | 74 +- imagecatalog/ExifReader.cs | 1190 ----------------- imagecatalog/ImageCatalog 2.csproj | 1 + .../Mappings/DataModelMappingProfile.cs | 50 +- imagecatalog/Models/SettingsDto.cs | 5 - imagecatalog/Program.cs | 8 +- 31 files changed, 241 insertions(+), 2597 deletions(-) delete mode 100644 MaddoShared/ImageCreatorGDI.cs delete mode 100644 MaddoShared/ImageCreatorMapper.cs delete mode 100644 imagecatalog/ExifReader.cs diff --git a/.forgejo/workflows/build-windows-avalonia.yml b/.forgejo/workflows/build-windows-avalonia.yml index 9fb522e..c418d92 100644 --- a/.forgejo/workflows/build-windows-avalonia.yml +++ b/.forgejo/workflows/build-windows-avalonia.yml @@ -91,6 +91,13 @@ jobs: exit 1 fi + legacy_renderer_count="$(find "${{ env.PUBLISH_DIR }}" -maxdepth 1 -type f \( -iname 'Microsoft.Windows.Compatibility.dll' -o -iname 'System.Private.Windows.GdiPlus.dll' \) | wc -l | tr -d ' ')" + if [ "${legacy_renderer_count}" -ne 0 ]; then + echo "Legacy GDI compatibility assemblies must not be published:" + find "${{ env.PUBLISH_DIR }}" -maxdepth 1 -type f \( -iname 'Microsoft.Windows.Compatibility.dll' -o -iname 'System.Private.Windows.GdiPlus.dll' \) -print + exit 1 + fi + - name: Upload publish artifact uses: actions/upload-artifact@v3 with: diff --git a/.forgejo/workflows/release-windows-avalonia.yml b/.forgejo/workflows/release-windows-avalonia.yml index 32dfd1f..ca7c521 100644 --- a/.forgejo/workflows/release-windows-avalonia.yml +++ b/.forgejo/workflows/release-windows-avalonia.yml @@ -82,6 +82,13 @@ jobs: exit 1 fi + legacy_renderer_count="$(find "${{ env.PUBLISH_DIR }}" -maxdepth 1 -type f \( -iname 'Microsoft.Windows.Compatibility.dll' -o -iname 'System.Private.Windows.GdiPlus.dll' \) | wc -l | tr -d ' ')" + if [ "${legacy_renderer_count}" -ne 0 ]; then + echo "Legacy GDI compatibility assemblies must not be published:" + find "${{ env.PUBLISH_DIR }}" -maxdepth 1 -type f \( -iname 'Microsoft.Windows.Compatibility.dll' -o -iname 'System.Private.Windows.GdiPlus.dll' \) -print + exit 1 + fi + - name: Upload publish artifact uses: actions/upload-artifact@v3 with: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3ce4bbe..8431a6c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,21 +42,17 @@ The main app launches Avalonia directly. Dialog events (`SelectSourceFolderReque 1. User configures paths/settings in the UI (`DataModel.cs` — MVVM ViewModel) 2. `ProcessImagesCommand` triggers `ImageCreationService` 3. `ImageCreationService` processes files in parallel chunks, with configurable concurrency and batch size (GC flush between chunks) -4. Each file is handled by an `IImageCreator` implementation (GDI+ or ImageSharp) +4. Each file is handled by the ImageSharp `IImageCreator` implementation 5. Output: resized/watermarked/overlaid images written to a destination folder hierarchy ### Key Abstractions (MaddoShared) -- **`IImageCreator`** — single async method to process one image; two implementations: `ImageCreatorGDI` (System.Drawing) and `ImageCreatorSharp` (SixLabors.ImageSharp) +- **`IImageCreator`** — single async method to process one image; implemented by `ImageCreatorImageSharp` (SixLabors.ImageSharp) - **`ImageCreationService`** — parallel orchestrator; uses `AsyncEnumerator` with chunking; loads logo once, clones per thread for thread safety - **`ImageState`** — per-file processing context (input path, EXIF orientation, thumbnail sizes, overlays, logo, rotation) -- **`PicSettings`** — 50+ property configuration model (dimensions, fonts, colors, JPEG quality, watermark, logo positioning, `ImageCreatorProvider` selector) +- **`PicSettings`** — 50+ property configuration model (dimensions, fonts, colors, JPEG quality, watermark, logo positioning) - **`FileHelperSharp`** — recursive file enumeration with folder-per-N-files mapping and counter formatting -### Implementation Selection - -`PicSettings.ImageCreatorProvider` switches between `"Sharp"` (SixLabors.ImageSharp) and `"Alternate"` (GDI+) at runtime. - ## Conventions ### C# Style diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 050a0aa..04a76b7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -88,6 +88,13 @@ build_windows: # Produce a single-file, ready-to-run publish so downstream jobs only need the EXE. try { & $dotnetExe publish "imagecatalog\ImageCatalog 2.csproj" -c $env:BUILD_CONFIG -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -p:PublishReadyToRun=true -o "imagecatalog\bin\$env:BUILD_CONFIG\net10.0-windows\publish" -v minimal + $publishDir = "imagecatalog\bin\$env:BUILD_CONFIG\net10.0-windows\publish" + $legacyRendererFiles = Get-ChildItem $publishDir -File | Where-Object { $_.Name -in @('Microsoft.Windows.Compatibility.dll', 'System.Private.Windows.GdiPlus.dll') } + if ($legacyRendererFiles) { + Write-Host 'Legacy GDI compatibility assemblies must not be published:' + $legacyRendererFiles | ForEach-Object { Write-Host $_.FullName } + exit 1 + } } catch { Write-Host "dotnet publish failed: $_" throw diff --git a/CatalogLite/CatalogConfigurationLoader.cs b/CatalogLite/CatalogConfigurationLoader.cs index 966e1dc..f7652c9 100644 --- a/CatalogLite/CatalogConfigurationLoader.cs +++ b/CatalogLite/CatalogConfigurationLoader.cs @@ -1,7 +1,8 @@ -using System.Drawing; using System.Globalization; using System.Xml.Linq; using MaddoShared; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace CatalogLite; @@ -89,7 +90,7 @@ public sealed class CatalogConfigurationLoader settings.Margine = values.GetInt("TestoMargine", 8); settings.LogoAltezza = values.GetInt("MarchioAltezza", 430); settings.LogoLarghezza = values.GetInt("MarchioLarghezza", 430); - settings.FontColoreRGB = ParseColor(values.GetString("ColoreTestoRGB", "Yellow"), Color.Yellow); + settings.FontColoreRGB = ParseColor(values.GetString("ColoreTestoRGB", "Yellow"), new Rgba32(255, 255, 0, 255)); settings.LogoAggiungi = values.GetBool("MarchioAggiungi"); settings.LogoNomeFile = values.GetString("MarchioFile"); settings.LogoTrasparenza = values.GetInt("MarchioTrasparenza", 100).ToString(CultureInfo.InvariantCulture); @@ -118,7 +119,6 @@ public sealed class CatalogConfigurationLoader settings.FotoRuotaASinistra = false; settings.TempMinText = string.Empty; settings.OverwriteFiles = values.GetBool("GeneraleSovrascriviFile"); - settings.ImageCreatorProvider = "ImageSharp"; } private static ImageCreationService.Options BuildOptions(ConfigurationValues values, string sourcePath, string destinationPath) @@ -150,7 +150,7 @@ public sealed class CatalogConfigurationLoader return string.Equals(values.GetString("MiniatureModalita"), mode, StringComparison.OrdinalIgnoreCase); } - private static Color ParseColor(string value, Color fallback) + private static Rgba32 ParseColor(string value, Rgba32 fallback) { if (string.IsNullOrWhiteSpace(value)) { @@ -162,14 +162,19 @@ public sealed class CatalogConfigurationLoader { if (normalized.StartsWith('#') && normalized.Length == 7) { - return Color.FromArgb( - Convert.ToInt32(normalized[1..3], 16), - Convert.ToInt32(normalized[3..5], 16), - Convert.ToInt32(normalized[5..7], 16)); + return new Rgba32( + Convert.ToByte(normalized[1..3], 16), + Convert.ToByte(normalized[3..5], 16), + Convert.ToByte(normalized[5..7], 16), + 255); } - var named = Color.FromName(normalized); - return named.IsKnownColor || named.IsNamedColor ? named : fallback; + if (normalized.Length == 6 && normalized.All(Uri.IsHexDigit)) + { + normalized = "#" + normalized; + } + + return Color.Parse(normalized).ToPixel(); } catch { diff --git a/CatalogLite/CatalogLite.csproj b/CatalogLite/CatalogLite.csproj index 7ad0f07..54121b7 100644 --- a/CatalogLite/CatalogLite.csproj +++ b/CatalogLite/CatalogLite.csproj @@ -8,7 +8,6 @@ CatalogLite false 2026-12-31 - false true false true @@ -27,7 +26,7 @@ - + diff --git a/CatalogLite/LiteCatalogViewModel.cs b/CatalogLite/LiteCatalogViewModel.cs index 56a87be..0a4c8c6 100644 --- a/CatalogLite/LiteCatalogViewModel.cs +++ b/CatalogLite/LiteCatalogViewModel.cs @@ -250,7 +250,6 @@ public sealed class LiteCatalogViewModel : ViewModelBase _picSettings.DirectorySorgente = SourcePath; _picSettings.DirectoryDestinazione = DestinationPath; _picSettings.DestDir = new DirectoryInfo(DestinationPath); - _picSettings.ImageCreatorProvider = "ImageSharp"; IsProcessing = true; ResetProgress("Analisi immagini..."); diff --git a/MaddoShared.Benchmarks/ChunkSizeBenchmarks.cs b/MaddoShared.Benchmarks/ChunkSizeBenchmarks.cs index 710b5e6..390799d 100644 --- a/MaddoShared.Benchmarks/ChunkSizeBenchmarks.cs +++ b/MaddoShared.Benchmarks/ChunkSizeBenchmarks.cs @@ -50,7 +50,7 @@ public class ChunkSizeBenchmarks }); var logger = loggerFactory.CreateLogger(); - var imageCreatorLogger = loggerFactory.CreateLogger(); + var imageCreatorLogger = loggerFactory.CreateLogger(); _picSettings = new PicSettings { @@ -75,7 +75,7 @@ public class ChunkSizeBenchmarks Trasparenza = 100 }; - var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger); + var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger); _imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService); } diff --git a/MaddoShared.Benchmarks/Helpers/TestImageGenerator.cs b/MaddoShared.Benchmarks/Helpers/TestImageGenerator.cs index 34f44b5..5630655 100644 --- a/MaddoShared.Benchmarks/Helpers/TestImageGenerator.cs +++ b/MaddoShared.Benchmarks/Helpers/TestImageGenerator.cs @@ -1,40 +1,32 @@ using System; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; namespace MaddoShared.Benchmarks.Helpers; /// -/// Helper class to generate test images for benchmarking +/// Helper class to generate test images for benchmarking. /// -[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public static class TestImageGenerator { - /// - /// Generates a set of test JPEG images in the specified directory - /// - /// Directory where images will be created - /// Number of images to generate - /// Width of each image - /// Height of each image - /// Whether to create images in subfolders public static void GenerateTestImages( - string outputDirectory, - int imageCount, - int width = 4000, + string outputDirectory, + int imageCount, + int width = 4000, int height = 3000, bool includeSubfolders = false) { Directory.CreateDirectory(outputDirectory); - var random = new Random(42); // Fixed seed for reproducibility + var random = new Random(42); + var encoder = new JpegEncoder { Quality = 85 }; - for (int i = 0; i < imageCount; i++) + for (var i = 0; i < imageCount; i++) { var targetDir = outputDirectory; - + if (includeSubfolders && i % 10 == 0) { targetDir = Path.Combine(outputDirectory, $"Subfolder_{i / 10}"); @@ -42,48 +34,17 @@ public static class TestImageGenerator } var filePath = Path.Combine(targetDir, $"test_image_{i:D5}.jpg"); - - // Skip if already exists if (File.Exists(filePath)) - continue; - - using var bitmap = new Bitmap(width, height); - using var graphics = Graphics.FromImage(bitmap); - - // Fill with a random color background - var bgColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); - graphics.Clear(bgColor); - - // Draw some random shapes to make it more realistic - for (int j = 0; j < 20; j++) { - var color = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); - var brush = new SolidBrush(color); - var x = random.Next(width); - var y = random.Next(height); - var w = random.Next(200, 800); - var h = random.Next(200, 800); - graphics.FillEllipse(brush, x, y, w, h); + continue; } - // Add some text - using var font = new Font("Arial", 48, FontStyle.Bold); - var text = $"Test Image {i}"; - var textBrush = new SolidBrush(Color.White); - graphics.DrawString(text, font, textBrush, new PointF(100, 100)); - - // Save as JPEG with standard quality - var encoder = GetEncoder(ImageFormat.Jpeg); - var encoderParameters = new EncoderParameters(1); - encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 85L); - - bitmap.Save(filePath, encoder, encoderParameters); + using var image = new Image(width, height, RandomColor(random)); + AddBenchmarkTexture(image, random); + image.Save(filePath, encoder); } } - /// - /// Cleans up generated test images - /// public static void CleanupTestImages(string directory) { if (Directory.Exists(directory)) @@ -92,16 +53,38 @@ public static class TestImageGenerator } } - private static ImageCodecInfo GetEncoder(ImageFormat format) + private static void AddBenchmarkTexture(Image image, Random random) { - var codecs = ImageCodecInfo.GetImageEncoders(); - foreach (var codec in codecs) + image.ProcessPixelRows(accessor => { - if (codec.FormatID == format.Guid) + for (var shape = 0; shape < 20; shape++) { - return codec; + var color = RandomColor(random); + var startX = random.Next(image.Width); + var startY = random.Next(image.Height); + var width = random.Next(200, Math.Min(800, image.Width) + 1); + var height = random.Next(200, Math.Min(800, image.Height) + 1); + var endX = Math.Min(accessor.Width, startX + width); + var endY = Math.Min(accessor.Height, startY + height); + + for (var y = startY; y < endY; y++) + { + var row = accessor.GetRowSpan(y); + for (var x = startX; x < endX; x++) + { + row[x] = color; + } + } } - } - return null; + }); + } + + private static Rgba32 RandomColor(Random random) + { + return new Rgba32( + (byte)random.Next(256), + (byte)random.Next(256), + (byte)random.Next(256), + 255); } } diff --git a/MaddoShared.Benchmarks/ImageProcessingBenchmarks.cs b/MaddoShared.Benchmarks/ImageProcessingBenchmarks.cs index 572c436..49f86d0 100644 --- a/MaddoShared.Benchmarks/ImageProcessingBenchmarks.cs +++ b/MaddoShared.Benchmarks/ImageProcessingBenchmarks.cs @@ -25,7 +25,7 @@ public class ImageProcessingBenchmarks private ImageCreationService _imageCreationStuff; private PicSettings _picSettings; private ILogger _logger; - private ILogger _imageCreatorLogger; + private ILogger _imageCreatorLogger; [Params(10, 50, 100)] public int ImageCount { get; set; } @@ -55,7 +55,7 @@ public class ImageProcessingBenchmarks }); _logger = loggerFactory.CreateLogger(); - _imageCreatorLogger = loggerFactory.CreateLogger(); + _imageCreatorLogger = loggerFactory.CreateLogger(); // Setup PicSettings with default values _picSettings = new PicSettings @@ -81,7 +81,7 @@ public class ImageProcessingBenchmarks Trasparenza = 100 }; - var imageCreatorService = new ImageCreatorGDI(_picSettings, _imageCreatorLogger); + var imageCreatorService = new ImageCreatorImageSharp(_picSettings, _imageCreatorLogger); _imageCreationStuff = new ImageCreationService(_logger, _picSettings, imageCreatorService); } diff --git a/MaddoShared.Benchmarks/ImageSizeBenchmarks.cs b/MaddoShared.Benchmarks/ImageSizeBenchmarks.cs index 4d71e9f..b190f8d 100644 --- a/MaddoShared.Benchmarks/ImageSizeBenchmarks.cs +++ b/MaddoShared.Benchmarks/ImageSizeBenchmarks.cs @@ -59,7 +59,7 @@ public class ImageSizeBenchmarks }); var logger = loggerFactory.CreateLogger(); - var imageCreatorLogger = loggerFactory.CreateLogger(); + var imageCreatorLogger = loggerFactory.CreateLogger(); _picSettings = new PicSettings { @@ -84,7 +84,7 @@ public class ImageSizeBenchmarks Trasparenza = 100 }; - var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger); + var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger); _imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService); } diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj index 56b8684..f68e319 100644 --- a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj +++ b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj @@ -2,10 +2,8 @@ Exe - net10.0-windows + net10.0 x64 - true - true diff --git a/MaddoShared.Benchmarks/StressTestBenchmark.cs b/MaddoShared.Benchmarks/StressTestBenchmark.cs index 207b434..32d1723 100644 --- a/MaddoShared.Benchmarks/StressTestBenchmark.cs +++ b/MaddoShared.Benchmarks/StressTestBenchmark.cs @@ -40,7 +40,7 @@ public class StressTestBenchmark Console.WriteLine($"[STRESS TEST] Generating {ImageCount} test images..."); Console.WriteLine("This may take several minutes depending on your hardware."); - + // Use smaller images for stress test to save space and time TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 1920, height: 1080); @@ -50,7 +50,7 @@ public class StressTestBenchmark }); var logger = loggerFactory.CreateLogger(); - var imageCreatorLogger = loggerFactory.CreateLogger(); + var imageCreatorLogger = loggerFactory.CreateLogger(); _picSettings = new PicSettings { @@ -75,7 +75,7 @@ public class StressTestBenchmark Trasparenza = 100 }; - var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger); + var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger); _imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService); Console.WriteLine($"[STRESS TEST] Setup complete. Ready to process {ImageCount} images."); @@ -130,12 +130,12 @@ public class StressTestBenchmark var results = new ConcurrentBag(); var startTime = DateTime.Now; - + await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None); - + var duration = DateTime.Now - startTime; var throughput = ImageCount / duration.TotalSeconds; - + Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s"); Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second"); } @@ -160,12 +160,12 @@ public class StressTestBenchmark var results = new ConcurrentBag(); var startTime = DateTime.Now; - + await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None); - + var duration = DateTime.Now - startTime; var throughput = ImageCount / duration.TotalSeconds; - + Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s"); Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second"); } diff --git a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj index 6a7b6fe..9d7d59a 100644 --- a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj +++ b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/MaddoShared.Tests/ImageCreatorSharpTests.cs b/MaddoShared.Tests/ImageCreatorSharpTests.cs index ae10edc..546a7de 100644 --- a/MaddoShared.Tests/ImageCreatorSharpTests.cs +++ b/MaddoShared.Tests/ImageCreatorSharpTests.cs @@ -1,237 +1,63 @@ -using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; using System.Reflection; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Shouldly; using MaddoShared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using SixLabors.ImageSharp; -namespace MaddoShared.Tests +namespace MaddoShared.Tests; + +[TestClass] +public class ImageCreatorSharpTests { - [TestClass] - public class ImageCreatorSharpTests + [TestMethod] + public void CalculateThumbnailSize_Larghezza_UsesWidthScaling() { - private ImageCreatorGDI CreateService(Action customize = null) + var size = CalculateThumbnailSize(400, 200, 200, "Larghezza"); + + size.Width.ShouldBe(200); + size.Height.ShouldBe(100); + } + + [TestMethod] + public void CalculateThumbnailSize_Altezza_UsesHeightScaling() + { + var size = CalculateThumbnailSize(200, 400, 200, "Altezza"); + + size.Width.ShouldBe(100); + size.Height.ShouldBe(200); + } + + [TestMethod] + public void FindBestFontSize_ConstrainsTextToBounds() + { + const string text = "A very long text that will not fit at the requested size"; + var method = typeof(ImageCreatorImageSharp).GetMethod( + "FindBestFontSize", + BindingFlags.NonPublic | BindingFlags.Static); + + method.ShouldNotBeNull(); + + var size = (float)method.Invoke(null, new object[] { - var settings = new PicSettings - { - DimStandard = 20, - DimStandardMiniatura = 10, - LarghezzaSmall = 100, - AltezzaSmall = 100, - LarghezzaBig = 800, - AltezzaBig = 600, - Trasparenza = 50, - IlFont = "Arial", - Grassetto = false, - Posizione = "CENTRO", - Allineamento = "CENTRO", - Margine = 10, - MargVert = 10, - TestoMin = false, - AggNumTempMin = false - }; + text, + "Arial", + 40, + 50f, + 20f, + 6 + })!; - customize?.Invoke(settings); + size.ShouldBeInRange(6f, 40f); + (size * text.Length * 0.6f <= 50f || size <= 6f).ShouldBeTrue(); + } - var logger = Substitute.For>(); - return new ImageCreatorGDI(settings, logger); - } + private static Size CalculateThumbnailSize(int width, int height, int maxPixel, string sizeMode) + { + var method = typeof(ImageCreatorImageSharp).GetMethod( + "CalculateThumbnailSize", + BindingFlags.NonPublic | BindingFlags.Static); - [TestMethod] - public void CalculateThumbnailSize_Larghezza_UsesWidthScaling() - { - var svc = CreateService(); - var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var size = (Size)mi.Invoke(svc, new object[] { 400, 200, 200, "Larghezza" }); - - size.Width.ShouldBe(200); - size.Height.ShouldBe(100); - } - - [TestMethod] - public void CalculateThumbnailSize_Altezza_UsesHeightScaling() - { - var svc = CreateService(); - var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var size = (Size)mi.Invoke(svc, new object[] { 200, 400, 200, "Altezza" }); - - size.Width.ShouldBe(100); - size.Height.ShouldBe(200); - } - - [TestMethod] - public void IsSameDirectory_IsCaseInsensitive() - { - var svc = CreateService(); - var mi = svc.GetType().GetMethod("IsSameDirectory", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - bool same = (bool)mi.Invoke(svc, new object[] { @"C:\Temp", @"c:\temp" }); - same.ShouldBeTrue(); - - bool notSame = (bool)mi.Invoke(svc, new object[] { @"C:\TempA", @"c:\temp" }); - notSame.ShouldBeFalse(); - } - - [TestMethod] - public void UpdateFilenameWithCode_InsertsCodeBeforeExtension() - { - var svc = CreateService(s => s.Codice = "_X"); - var mi = svc.GetType().GetMethod("UpdateFilenameWithCode", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var state = new ImageState { NomeFileSmall = "photo123.jpg" }; - mi.Invoke(svc, new object[] { state }); - - state.NomeFileSmall.ShouldBe("photo123_X.jpg"); - } - - [DataTestMethod] - [DataRow("SINISTRA")] - [DataRow("CENTRO")] - [DataRow("DESTRA")] - public void CalculateHorizontalAlignment_RespectsAlignment(string alignment) - { - var svc = CreateService(s => { s.Allineamento = alignment; s.Margine = 20; }); - - var mi = svc.GetType().GetMethod("CalculateHorizontalAlignment", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var center = (float)mi.Invoke(svc, new object[] { 800, 100f }); - - if (alignment == "SINISTRA") - center.ShouldBeInRange(0f, 400f); - if (alignment == "DESTRA") - center.ShouldBeInRange(400f, 800f); - if (alignment == "CENTRO") - center.ShouldBe(800 / 2f, 0.0001f); - } - - [TestMethod] - public void SetVerticalPosition_AltoAndBasso_SetExpectedValues() - { - var svc = CreateService(s => s.Posizione = "ALTO"); - var mi = svc.GetType().GetMethod("SetVerticalPosition", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var state = new ImageState(); - - // ALTO - mi.Invoke(svc, new object[] { 500, 20f, state }); - state.YPosFromBottom1.ShouldBe(10f); - state.YPosFromBottom4.ShouldBe(10f); - - // BASSO - state = new ImageState(); - svc = CreateService(s => { s.Posizione = "BASSO"; s.Margine = 10; s.MargVert = 5; }); - mi = svc.GetType().GetMethod("SetVerticalPosition", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Invoke(svc, new object[] { 200, 20f, state }); - - var expected1 = (float)(200 - 20 - (200 * 10 / 100.0)); - var expected4 = (float)(200 - 20 - (200 * 5 / 100.0)); - state.YPosFromBottom1.ShouldBe(expected1, 0.001f); - state.YPosFromBottom4.ShouldBe(expected4, 0.001f); - } - - [TestMethod] - public void FormatTimeText_WithAndWithoutFileName_ProducesExpectedStrings() - { - var svc = CreateService(); - var mi = svc.GetType().GetMethod("FormatTimeText", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var state = new ImageState - { - NomeFileBig = "file.jpg", - TestoOrario = "T:", - DataPartenzaI = new DateTime(2024, 01, 01, 12, 0, 0), - DataFoto = new DateTime(2024, 01, 01, 11, 59, 0) - }; - var withoutName = (string)mi.Invoke(svc, new object[] { state, false }); - withoutName.ShouldStartWith(Environment.NewLine); - withoutName.ShouldContain("T:"); - - var withName = (string)mi.Invoke(svc, new object[] { state, true }); - withName.ShouldContain("file.jpg"); - withName.ShouldContain("T:"); - withName.ShouldContain(Environment.NewLine); - } - - [TestMethod] - public void PrepareSignatureText_SetsSmallSignature_AccordingFlags() - { - var svc = CreateService(); - var miPrep = svc.GetType().GetMethod("PrepareSignatureText", BindingFlags.NonPublic | BindingFlags.Instance); - miPrep.ShouldNotBeNull(); - - var state = new ImageState { NomeFileBig = "bigname.jpg" }; - - svc = CreateService(s => s.TestoMin = true); - miPrep.Invoke(svc, new object[] { state }); - state.TestoFirmaPiccola.ShouldBe("bigname.jpg"); - - state.TestoFirmaPiccola = ""; - svc = CreateService(s => { s.TestoMin = false; s.AggNumTempMin = true; }); - miPrep.Invoke(svc, new object[] { state }); - state.TestoFirmaPiccola.ShouldBe("bigname.jpg "); - } - - [TestMethod] - public void ShouldRenderText_ReturnsCorrectFlag() - { - var svc = CreateService(s => { s.UsaOrarioMiniatura = false; s.TestoMin = false; s.AggTempoGaraMin = false; s.AggNumTempMin = false; }); - var mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance); - mi.ShouldNotBeNull(); - - var res = (bool)mi.Invoke(svc, Array.Empty()); - res.ShouldBeFalse(); - - svc = CreateService(s => s.TestoMin = true); - mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance); - res = (bool)mi.Invoke(svc, Array.Empty()); - res.ShouldBeTrue(); - } - - [TestMethod] - public void FindBestFontSize_And_AdjustFontToFitWidth_ModifySizes() - { - var svc = CreateService(s => { s.IlFont = "Arial"; s.DimStandardMiniatura = 30; }); - - using var bmp = new Bitmap(400, 100); - using var g = Graphics.FromImage(bmp); - - var miFind = svc.GetType().GetMethod("FindBestFontSize", BindingFlags.NonPublic | BindingFlags.Instance); - miFind.ShouldNotBeNull(); - - int best = (int)miFind.Invoke(svc, new object[] { g, "A very long text that won't fit", "Arial", 40, false, 50, 5 }); - best.ShouldBeInRange(5, 40); - - // The helper AdjustFontToFitWidth was in an earlier refactor; replicate its logic here - var imageState = new ImageState { DimensioneStandardMiniatura = 30, TestoFirmaPiccola = "A very long test string" }; - var initialFont = new Font("Arial", imageState.DimensioneStandardMiniatura); - var textSize = g.MeasureString(imageState.TestoFirmaPiccola, initialFont); - - int tempFontSize = imageState.DimensioneStandardMiniatura; - while ((textSize.Width > 50) && tempFontSize > 5) - { - tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1; - using var tempFont = new Font("Arial", tempFontSize); - textSize = g.MeasureString(imageState.TestoFirmaPiccola, tempFont); - } - - var updatedSize = textSize; - imageState.DimensioneStandardMiniatura = tempFontSize; - - imageState.DimensioneStandardMiniatura.ShouldBeLessThanOrEqualTo(30); - (updatedSize.Width <= 50 || imageState.DimensioneStandardMiniatura <= 5).ShouldBeTrue(); - } + method.ShouldNotBeNull(); + return (Size)method.Invoke(null, new object[] { width, height, maxPixel, sizeMode })!; } } diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index 2c8d370..0060244 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -18,7 +18,6 @@ - diff --git a/MaddoShared/ImageCreatorGDI.cs b/MaddoShared/ImageCreatorGDI.cs deleted file mode 100644 index 15fd6f6..0000000 --- a/MaddoShared/ImageCreatorGDI.cs +++ /dev/null @@ -1,866 +0,0 @@ -#if WINDOWS -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Globalization; -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; - -// Imports System.Threading - -namespace MaddoShared; - -[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] -public class ImageCreatorGDI(PicSettings picSettings, ILogger logger) : IImageCreator -{ - public async Task CreateImageAsync(ImageState imgState, byte[]? logoData) - { - try - { - await Task.Run(() => - { - logger.LogInformation("File: {FileInfo} Dest: {DirectoryInfo}", imgState.WorkFile, imgState.DestDir); - PrepareVariables(imgState); - ExtractExif(imgState); - - using var g = Image.FromFile(imgState.WorkFile.FullName); - - // Set extra text - SetExtraText(g, imgState); - - // Rotate image according to EXIF - ApplyRotation(g, imgState); - - // Force jpeg if option selected - var thisFormat = g.RawFormat; - if (picSettings.UsaForzaJpg) - thisFormat = ImageFormat.Jpeg; - - PrepareThumbnailSize(g, imgState); - - using var imgOutputBig = new Bitmap(g, imgState.ThumbSizeBig.Width, imgState.ThumbSizeBig.Height); - - imgOutputBig.SetResolution(g.HorizontalResolution, g.VerticalResolution); - - // Create thumbnails - CreateThumbnails(g, imgState, imgOutputBig, thisFormat); - - AddText(g, imgState, imgOutputBig); - - AddLogo(imgOutputBig, logoData); - - SavePhoto(imgOutputBig, imgState, thisFormat); - }).ConfigureAwait(false); - } - - catch (Exception ex) - { - var e = ex.Demystify(); - logger.LogError(e, "Error in processing photo {WorkFileName}", imgState.WorkFile.Name); - } - } - - private void ExtractExif(ImageState imgState) - { - using var img = SixLabors.ImageSharp.Image.Load(imgState.WorkFile.FullName); - imgState.Orientation = Orientations.TopLeft; - - IExifValue rotation = null; - - var exifProfile = img.Metadata?.ExifProfile; - var found = exifProfile != null && exifProfile.TryGetValue(ExifTag.Orientation, out rotation); - - if (found) - { - var intOrientation = rotation.Value.ToInt32(); - imgState.Orientation = (Orientations)intOrientation; - } - - IExifValue date = null; - var creationFound = exifProfile != null && exifProfile.TryGetValue(ExifTag.DateTimeOriginal, out date); - if (creationFound) - { - var succ = DateTime.TryParseExact(date.Value, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture, - DateTimeStyles.None, out var crDate); - if (succ) - { - imgState.CreationDate = crDate; - } - else - { - imgState.CreationDate = null; - } - } - else - { - imgState.CreationDate = null; - } - } - - private void ApplyRotation(Image g, ImageState imgState) - { - imgState.FotoRuotaADestra = false; - imgState.FotoRuotaASinistra = false; - - if (picSettings.UsaRotazioneAutomatica && g.PropertyIdList.Length > 0) - { - switch (imgState.Orientation) - { - case Orientations.BottomLeft: - case Orientations.BottomRight: - case Orientations.LeftTop: - case Orientations.LftBottom: - imgState.FotoRuotaASinistra = true; - break; - case Orientations.RightBottom: - case Orientations.RightTop: - case Orientations.TopLeft: - case Orientations.TopRight: - break; - } - } - - if (imgState.FotoRuotaASinistra) - g.RotateFlip(RotateFlipType.Rotate270FlipNone); - if (imgState.FotoRuotaADestra) - g.RotateFlip(RotateFlipType.Rotate90FlipNone); - } - - /// - /// ''' Aggiunge Orario, tempo gara e altri - /// ''' - /// ''' Image - /// - /// ''' - private void SetExtraText(Image g, ImageState imgState) - { - if (picSettings.UsaOrarioTestoApplicare || picSettings.UsaTempoGaraTestoApplicare || - picSettings.UsaOrarioMiniatura || picSettings.TestoMin || picSettings.AggTempoGaraMin || - picSettings.AggNumTempMin) - { - if (g.PropertyIdList.Length <= 0) return; - imgState.DataFoto = imgState.CreationDate ?? DateTime.Now; - imgState.TestoFirma = picSettings.TestoFirmaStart; - imgState.TestoFirmaV = picSettings.TestoFirmaStartV; - - if (imgState.DataFoto.Year == 1) return; - imgState.TestoFirmaPiccola = imgState.DataFoto.ToShortTimeString(); - if (picSettings.UsaOrarioTestoApplicare) - { - imgState.TestoFirma += - $" {imgState.DataFoto.ToShortDateString()} {imgState.DataFoto.ToLongTimeString()}"; - imgState.TestoFirmaV += - $" {imgState.DataFoto.ToShortDateString()} {imgState.DataFoto.ToLongTimeString()}"; - } - - if (!picSettings.UsaTempoGaraTestoApplicare) return; - var diff = imgState.DataFoto - imgState.DataPartenzaI; - imgState.TestoFirma += $" {imgState.TestoOrario}{diff.Hours:00}:{diff.Minutes:00}:{diff.Seconds:00}"; - imgState.TestoFirmaV += $" {imgState.TestoOrario}{diff.Hours:00}:{diff.Minutes:00}:{diff.Seconds:00}"; - } - else - { - imgState.TestoFirma = picSettings.TestoFirmaStart; - imgState.TestoFirmaV = picSettings.TestoFirmaStartV; - } - } - - /// - /// ''' Prepara diverse variabili azzerandole, elaborandole e prendendole dalle impostazioni - /// ''' - /// ''' - private void PrepareVariables(ImageState imgState) - { - imgState.AlphaScelta = System.Convert.ToInt32((255 * (100 - picSettings.Trasparenza) / (double)100)); - imgState.TestoFirma = ""; - imgState.TestoFirmaV = ""; - imgState.DataPartenzaI = picSettings.DataPartenza; - imgState.TestoOrario = picSettings.TestoOrario; - if (imgState.TestoOrario.Length > 0) - imgState.TestoOrario += " "; - imgState.TestoFirmaPiccola = ""; - imgState.ThumbSizeSmall = new Size(); - imgState.ThumbSizeBig = new Size(); - imgState.NomeFileSmall = ""; - imgState.NomeFileBig2 = ""; - imgState.NomeFileBig = ""; - imgState.DimensioneStandard = picSettings.DimStandard; - imgState.DimensioneStandardMiniatura = picSettings.DimStandardMiniatura; - // nomeFileSmall = Suffisso & NomeFileChild - // nomeFileBig = NomeFileChild - imgState.NomeFileSmall = picSettings.Suffisso + imgState.WorkFile.Name; - imgState.NomeFileBig = imgState.WorkFile.Name; - // Sanitize file names to avoid invalid characters causing IO errors - imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall); - imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig); - } - - private static string SanitizeFileName(string fileName) - { - if (string.IsNullOrEmpty(fileName)) return fileName; - var invalid = 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 PrepareThumbnailSize(Image g, ImageState imgState) - { - if (g.Width > g.Height) - { - imgState.ThumbSizeSmall = CalculateThumbnailSize(g.Width, g.Height, picSettings.LarghezzaSmall, "Larghezza"); - var sizeOrig = new Size(g.Width, g.Height); - imgState.ThumbSizeBig = sizeOrig; - } - else - { - imgState.ThumbSizeSmall = CalculateThumbnailSize(g.Width, g.Height, picSettings.AltezzaSmall, "Altezza"); - var sizeOrig = new Size(g.Width, g.Height); - imgState.ThumbSizeBig = sizeOrig; - } - } - - private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format) - { - // Only skip thumbnail generation when the global "create thumbnails" flag is false. - // Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText - if (!picSettings.CreaMiniature) - return; - - PrepareSignatureText(imgState); - - if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione)) - UpdateFilenameWithCode(imgState); - - if (ShouldRenderText()) - CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format); - else - CreateSimpleThumbnail(sourceImage, imgState, format); - } - - private void PrepareSignatureText(ImageState imgState) - { - if (picSettings.TestoMin) - imgState.TestoFirmaPiccola = imgState.NomeFileBig; - else if (picSettings.AggNumTempMin) - imgState.TestoFirmaPiccola = imgState.NomeFileBig + " "; - } - - private bool IsSameDirectory(string dir1, string dir2) => - string.Equals(dir1, dir2, StringComparison.OrdinalIgnoreCase); - - private void UpdateFilenameWithCode(ImageState imgState) - { - var name = imgState.NomeFileSmall; - imgState.NomeFileSmall = name[..^4] + picSettings.Codice + name[^4..]; - } - - private bool ShouldRenderText() => - picSettings.UsaOrarioMiniatura || picSettings.TestoMin || picSettings.AggTempoGaraMin || - picSettings.AggNumTempMin; - - private void CreateSimpleThumbnail(Image image, ImageState imgState, ImageFormat format) - { - using var thumbnail = new Bitmap(image, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height); - thumbnail.Save(Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall), format); - } - - private void CreateThumbnailWithText(Image image, ImageState imgState, Bitmap sourceBitmap, ImageFormat format) - { - if (imgState.TestoFirmaPiccola.Length == 0) - { - CreateSimpleThumbnail(image, imgState, format); - return; - } - - using var imgOutputSmall = (Bitmap)sourceBitmap.Clone(); - using var graphics = Graphics.FromImage(imgOutputSmall); - graphics.SmoothingMode = SmoothingMode.AntiAlias; - - // Use the user's configured font size directly - using var font1 = CreateFont(picSettings.IlFont, imgState.DimensioneStandardMiniatura, picSettings.Grassetto); - var textSize = graphics.MeasureString(imgState.TestoFirmaPiccola, font1); - - // Adjust font if it's too large for the image dimensions - // Keep text height under 15% of image height to ensure proper spacing when resized - // This leaves room for margins and prevents clipping - int tempFontSize = imgState.DimensioneStandardMiniatura; - float maxTextHeight = image.Height * 0.15f; - - while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5) - { - tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1; - using var tempFont = CreateFont(picSettings.IlFont, tempFontSize, picSettings.Grassetto); - textSize = graphics.MeasureString(imgState.TestoFirmaPiccola, tempFont); - } - - // Re-measure text with the final font size for accurate positioning - using var finalFont = CreateFont(picSettings.IlFont, tempFontSize, picSettings.Grassetto); - var finalTextSize = graphics.MeasureString(imgState.TestoFirmaPiccola, finalFont); - - SetVerticalPosition(image.Height, finalTextSize.Height, imgState); - - float xCenter = CalculateHorizontalAlignment(image.Width, finalTextSize.Width); - using var stringFormat = new StringFormat(); - stringFormat.Alignment = StringAlignment.Center; - - using var shadowBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, 0, 0, 0)); - using var textBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, picSettings.FontColoreRGB)); - - DrawText(graphics, imgState, xCenter, stringFormat, shadowBrush, textBrush, finalFont); - - using var finalThumb = - new Bitmap(imgOutputSmall, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height); - finalThumb.Save(Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall), format); - } - - private Font CreateFont(string fontName, int size, bool bold) => - new Font(fontName, size, bold ? FontStyle.Bold : FontStyle.Regular); - - private int FindBestFontSize(Graphics g, string text, string fontName, int maxSize, bool bold, int maxWidth, int minSize = 5) - { - 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; - using var testFont = CreateFont(fontName, mid, bold); - var measured = g.MeasureString(text, testFont); - if (measured.Width <= maxWidth) - { - best = mid; - low = mid + 1; // try larger - } - else - { - high = mid - 1; // too big - } - } - - return best; - } - - - private void AddText(Image g, ImageState imgState, Bitmap imgOutputBig) - { - using var grPhoto = Graphics.FromImage(imgOutputBig); - grPhoto.SmoothingMode = SmoothingMode.AntiAlias; - - // Determine best base font size using a binary search (faster than decremental loop) - int availableWidth = (int)g.Width; - int targetBaseSize = imgState.DimensioneStandard > 0 ? imgState.DimensioneStandard : picSettings.DimStandard; - int bestBaseSize = FindBestFontSize(grPhoto, imgState.TestoFirma ?? string.Empty, picSettings.IlFont, targetBaseSize, picSettings.Grassetto, availableWidth); - imgState.DimensioneStandard = bestBaseSize; - - // Decide final drawing size (use DimVert if rotated) - int drawSize = (imgState.FotoRuotaADestra || imgState.FotoRuotaASinistra) ? picSettings.DimVert : imgState.DimensioneStandard; - - using var drawFont = CreateFont(picSettings.IlFont, drawSize, picSettings.Grassetto); - var crSize = grPhoto.MeasureString(imgState.TestoFirma ?? string.Empty, drawFont); - var larghezzaStandard = Convert.ToInt32(crSize.Width); - - // Vertical positions - switch (picSettings.Posizione.ToUpper()) - { - case "ALTO": - { - imgState.YPosFromBottom = picSettings.Margine; - imgState.YPosFromBottom3 = picSettings.MargVert; - break; - } - - case "BASSO": - { - imgState.YPosFromBottom = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0))); - imgState.YPosFromBottom3 = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0))); - break; - } - } - - float xCenterOfImg = 0; - using var strFormat = new StringFormat(); - switch (picSettings.Allineamento.ToUpper()) - { - case "SINISTRA": - { - xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } - - case "CENTRO": - { - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } - - case "DESTRA": - { - xCenterOfImg = - Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } - } - - strFormat.Alignment = StringAlignment.Center; - - using var semiTransBrush2 = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, 0, 0, 0)); - using var semiTransBrush = new SolidBrush(Color.FromArgb(imgState.AlphaScelta, picSettings.FontColoreRGB)); - - // write text (NomeFileBig) - if (picSettings.TestoNome) - { - if (picSettings.NomeData && g.PropertyIdList.Length > 0) - { - imgState.DataFoto = imgState.CreationDate ?? DateTime.Now; - - grPhoto.DrawString((imgState.NomeFileBig + " " + imgState.DataFoto.ToShortDateString()), drawFont, - semiTransBrush2, new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat); - grPhoto.DrawString((imgState.NomeFileBig + " " + imgState.DataFoto.ToShortDateString()), drawFont, - semiTransBrush, new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat); - } - else - { - grPhoto.DrawString(imgState.NomeFileBig, drawFont, semiTransBrush2, - new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat); - grPhoto.DrawString(imgState.NomeFileBig, drawFont, semiTransBrush, - new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat); - } - } - else - { - if (imgState.FotoRuotaADestra || imgState.FotoRuotaASinistra) - { - if (!picSettings.TestoMin) - { - grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush2, - new PointF(xCenterOfImg + 1, imgState.YPosFromBottom3 + 1), strFormat); - grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush, - new PointF(xCenterOfImg, imgState.YPosFromBottom3), strFormat); - } - - if (picSettings.TestoMin) - { - grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush2, - new PointF(xCenterOfImg + 1, imgState.YPosFromBottom4 + 1), strFormat); - grPhoto.DrawString(imgState.TestoFirmaV, drawFont, semiTransBrush, - new PointF(xCenterOfImg, imgState.YPosFromBottom4), strFormat); - } - } - else - { - grPhoto.DrawString(imgState.TestoFirma, drawFont, semiTransBrush2, - new PointF(xCenterOfImg + 1, imgState.YPosFromBottom + 1), strFormat); - grPhoto.DrawString(imgState.TestoFirma, drawFont, semiTransBrush, - new PointF(xCenterOfImg, imgState.YPosFromBottom), strFormat); - } - } - - if (string.Equals(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione, - StringComparison.OrdinalIgnoreCase)) - { - imgState.NomeFileBig2 = imgState.NomeFileBig; - imgState.NomeFileBig = $"{imgState.NomeFileBig[..^4]}{picSettings.Codice}{imgState.NomeFileBig[^4..]}"; - } - } - - private void AddLogo(Bitmap imgOutputBig, byte[]? logoData) - { - // Skip if no logo bytes provided - if (logoData is null) 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. - using var grWatermark = Graphics.FromImage(imgOutputBig); - using ImageAttributes imageAttributes = new ImageAttributes(); - - if (picSettings.UseTransparentColor) - { - Color keyColor = Color.White; - try - { - if (!string.IsNullOrWhiteSpace(picSettings.TransparentColor)) - { - // ColorTranslator accepts both "#RRGGBB" and "RRGGBB" - keyColor = ColorTranslator.FromHtml(picSettings.TransparentColor); - } - } - catch - { - keyColor = Color.White; - } - - var colorMap = new ColorMap - { - // background: the color we search for and replace with transparency - OldColor = keyColor, - NewColor = Color.FromArgb(0, 0, 0, 0) - }; - - var remapTable = new[] { colorMap }; - imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap); - } - - // * The second color manipulation is used to change the opacity by setting the 3rd row and 3rd column to 0.3f - // Parse transparency safely (default to 100 if parsing fails) - if (!int.TryParse(picSettings.LogoTrasparenza, out var logoTransparencyValue)) - { - logoTransparencyValue = 100; - } - - var colorMatrixElements = new[] - { - new[] { 1.0F, 0.0F, 0.0F, 0.0F, 0.0F }, new[] { 0.0F, 1.0F, 0.0F, 0.0F, 0.0F }, - new[] { 0.0F, 0.0F, 1.0F, 0.0F, 0.0F }, - new[] { 0.0F, 0.0F, 0.0F, System.Convert.ToSingle(logoTransparencyValue) / 100F, 0.0F }, - new[] { 0.0F, 0.0F, 0.0F, 0.0F, 1.0F } - }; - var wmColorMatrix = new ColorMatrix(colorMatrixElements); - imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap); - - var fotoLogoH = picSettings.LogoAltezza; - var fotoLogoW = picSettings.LogoLarghezza; - var fattoreAlt = logo.Height / (double)fotoLogoH; - var fattoreLarg = logo.Width / (double)fotoLogoW; - var nuovaSize = fattoreLarg > fattoreAlt - ? CalculateThumbnailSize(logo.Width, logo.Height, fotoLogoW, "Larghezza") - : CalculateThumbnailSize(logo.Width, logo.Height, fotoLogoH, "Altezza"); - - // Guard against null/empty LogoMargine and percentage parsing - var logoMargineStr = picSettings.LogoMargine ?? string.Empty; - var inPercentualeL = logoMargineStr.EndsWith('%'); - var margineL = 0; - if (inPercentualeL) - { - var trimmed = logoMargineStr.TrimEnd('%'); - if (!int.TryParse(trimmed, out margineL)) margineL = 0; - } - else - { - if (!int.TryParse(logoMargineStr, out margineL)) margineL = 0; - } - var margineUsato = - inPercentualeL ? System.Convert.ToInt32(imgOutputBig.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 = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2); - break; - } - - case "DESTRA": - { - xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato); - break; - } - } - - switch (logoV) - { - case "ALTO": - case "NESSUNA": - { - yPosOfWm = margineUsato; - break; - } - - case "CENTRO": - { - yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2); - break; - } - - case "BASSO": - { - yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato); - break; - } - } - - grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0, - logo.Width, logo.Height, GraphicsUnit.Pixel, imageAttributes); - //grWatermark.Dispose(); - } - - - private void SavePhoto(Bitmap imgOutputBig, ImageState imgState, ImageFormat thisFormat) - { - var fileName = Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig); - - using var image1Stream = new MemoryStream(); - if (picSettings.FotoGrandeDimOrigina == false) - { - // attenzione non controlla se è png - // imgOutputBig.Save(Path.Combine(_DestDir.FullName, "Temp_" & NomeFileBig), thisFormat) - if (thisFormat.Equals(ImageFormat.Jpeg)) - { - MakeImageCustomQuality(imgOutputBig, image1Stream, picSettings.JpegQuality); - } - //SalvaImmagineCustomQuality(imgOutputBig, Path.Combine(DestDir.FullName, "Temp_" + NomeFileBig), _picSettings.jpegQuality); - else - { - imgOutputBig.Save(image1Stream, thisFormat); - } - - //imgOutputBig.Save(Path.Combine(DestDir.FullName, "Temp_" + NomeFileBig), thisFormat); - image1Stream.Seek(0, SeekOrigin.Begin); - using var g2 = Image.FromStream(image1Stream); - imgState.ThumbSizeBig = g2.Width > g2.Height - ? CalculateThumbnailSize(g2.Width, g2.Height, picSettings.LarghezzaBig, "Larghezza") - : CalculateThumbnailSize(g2.Width, g2.Height, picSettings.AltezzaBig, "Altezza"); - using var imgOutputBig2 = new Bitmap(g2, imgState.ThumbSizeBig.Width, imgState.ThumbSizeBig.Height); - - if (!picSettings.OverwriteFiles && File.Exists(fileName)) - { - logger.LogInformation("Saltata foto {FileName}, esiste", fileName); - } - else - { - if (thisFormat.Equals(ImageFormat.Jpeg)) - SaveImageCustomQuality(imgOutputBig2, fileName, picSettings.JpegQuality); - else - imgOutputBig2.Save(fileName, thisFormat); - } - } - else - { - if (!picSettings.OverwriteFiles && File.Exists(fileName)) - { - logger.LogInformation("Saltata foto {FileName}, esiste", fileName); - } - else - { - if (thisFormat.Equals(ImageFormat.Jpeg)) - SaveImageCustomQuality(imgOutputBig, fileName, picSettings.JpegQuality); - else - imgOutputBig.Save(fileName, thisFormat); - } - } - - image1Stream.Seek(0, SeekOrigin.Begin); - - if (!picSettings.CreaMiniature) return; - if (!picSettings.AggiungiScritteMiniature) return; - - using var g1 = picSettings.FotoGrandeDimOrigina ? (Image)imgOutputBig.Clone() : Image.FromStream(image1Stream); - - using var imgOutputSmall = new Bitmap(g1, imgState.ThumbSizeSmall.Width, imgState.ThumbSizeSmall.Height); - - if (string.Equals(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione, - StringComparison.OrdinalIgnoreCase)) - imgState.NomeFileSmall = imgState.NomeFileSmall.Substring(0, imgState.NomeFileSmall.Length - 4) + - picSettings.Codice + - imgState.NomeFileSmall.Substring(imgState.NomeFileSmall.Length - 4); - - var tnFileName = Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall); - - if (!picSettings.OverwriteFiles && File.Exists(tnFileName)) - { - logger.LogInformation("Saltata miniatura foto {TnFileName}, esiste", tnFileName); - } - else - { - if (thisFormat.Equals(ImageFormat.Jpeg)) - SaveImageCustomQuality(imgOutputSmall, tnFileName, picSettings.JpegQualityMin); - else - imgOutputSmall.Save(tnFileName, thisFormat); - } - } - - private void SaveImageCustomQuality(Bitmap imageToSave, string nomeFileFinale, long quality) - { - var jgpEncoder = GetEncoder(ImageFormat.Jpeg); - var myEncoder = System.Drawing.Imaging.Encoder.Quality; - - using var myEncoderParameters = new EncoderParameters(1); - - var myEncoderParameter = new EncoderParameter(myEncoder, quality); - myEncoderParameters.Param[0] = myEncoderParameter; - imageToSave.Save(nomeFileFinale, jgpEncoder, myEncoderParameters); - //imageToSave.Dispose(); - } - - private void MakeImageCustomQuality(Bitmap imageToSave, Stream destinationStream, long quality) - { - var jgpEncoder = GetEncoder(ImageFormat.Jpeg); - var myEncoder = System.Drawing.Imaging.Encoder.Quality; - - using var myEncoderParameters = new EncoderParameters(1); - - var myEncoderParameter = new EncoderParameter(myEncoder, quality); - myEncoderParameters.Param[0] = myEncoderParameter; - destinationStream.Seek(0, SeekOrigin.Begin); - imageToSave.Save(destinationStream, jgpEncoder, myEncoderParameters); - //imageToSave.Dispose(); - } - - private ImageCodecInfo GetEncoder(ImageFormat format) - { - var codecs = ImageCodecInfo.GetImageDecoders(); - - foreach (var codec in codecs) - { - if (codec.FormatID == format.Guid) - return codec; - } - - return null /* TODO Change to default(_) if this is not a reference type */; - } - - /// - /// ''' Calculate the Size of the New image - /// ''' - /// ''' Larghezza - /// ''' Altezza - /// ''' - /// ''' - /// ''' - /// ''' - private Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize) - { - // e - // *** Larghezza, Altezza, Auto - - double tempMultiplier; - - if (tipoSize.ToUpper() == "Larghezza".ToUpper()) - tempMultiplier = maxPixel / (double)currentwidth; - else if (tipoSize.ToUpper() == "Altezza".ToUpper()) - tempMultiplier = maxPixel / (double)currentheight; - else if (currentheight > currentwidth) - tempMultiplier = maxPixel / (double)currentheight; - else - tempMultiplier = maxPixel / (double)currentwidth; - - var newSize = new Size(System.Convert.ToInt32(currentwidth * tempMultiplier), - System.Convert.ToInt32(currentheight * tempMultiplier)); - - return newSize; - } - - private void SetVerticalPosition(int imgHeight, float textHeight, ImageState imgState) - { - // Use 1% of image height as minimum margin, or 10px, whichever is larger - float minMargin = Math.Max(10f, imgHeight * 0.01f); - - switch (picSettings.Posizione.ToUpper()) - { - case "ALTO": - imgState.YPosFromBottom1 = Math.Max(minMargin, picSettings.Margine); - imgState.YPosFromBottom4 = Math.Max(minMargin, picSettings.MargVert); - break; - - case "BASSO": - var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0); - var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0); - - // Position from bottom: bottom edge of text at desired margin from bottom - // Y = imageHeight - textHeight - bottomMargin - var desiredY1 = imgHeight - textHeight - bottomMargin1; - var desiredY4 = imgHeight - textHeight - bottomMargin4; - - // Ensure text stays completely within bounds: - // - Top edge must be >= minMargin (not clipped at top) - // - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom) - var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin - var maxAllowedY4 = imgHeight - textHeight - minMargin; - - imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1)); - imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4)); - break; - - case "CENTRO": - default: - // Center the text vertically - var centeredY = (imgHeight - textHeight) / 2f; - // Clamp to ensure margins are respected - imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(centeredY, imgHeight - textHeight - minMargin)); - imgState.YPosFromBottom4 = imgState.YPosFromBottom1; - break; - } - } - - private float CalculateHorizontalAlignment(int imgWidth, float textWidth) - { - double halfWidth = textWidth / 2.0; - - return picSettings.Allineamento.ToUpper() switch - { - "SINISTRA" => (float)Math.Min(picSettings.Margine + halfWidth, imgWidth / 2.0), - "DESTRA" => (float)Math.Max(imgWidth - picSettings.Margine - halfWidth, imgWidth / 2.0), - _ => imgWidth / 2.0f, // CENTRO or default - }; - } - - private void DrawText(Graphics g, ImageState imgState, float x, StringFormat format, - Brush shadowBrush, Brush textBrush, Font font) - { - string content = imgState.TestoFirmaPiccola; - - if (picSettings.TestoMin) - { - content = imgState.NomeFileBig; - } - else if (picSettings.AggTempoGaraMin && picSettings.UsaTempoGaraTestoApplicare) - { - content = FormatTimeText(imgState, includeFileName: false); - } - else if (picSettings.AggNumTempMin) - { - content = FormatTimeText(imgState, includeFileName: true); - } - - var offset = new PointF(x + 1, imgState.YPosFromBottom1 + 1); - var actual = new PointF(x, imgState.YPosFromBottom1); - - g.DrawString(content, font, shadowBrush, offset, format); - g.DrawString(content, font, textBrush, actual, format); - } - - private string FormatTimeText(ImageState imgState, bool includeFileName) - { - var diff = imgState.DataPartenzaI - imgState.DataFoto; - var ticks = (long)(diff.TotalSeconds * 10000000); - var time = new TimeSpan(ticks); - - var formatted = $"{imgState.TestoOrario}{time:hh\\:mm\\:ss}"; - return includeFileName - ? $"{imgState.NomeFileBig}{Environment.NewLine}{formatted}" - : Environment.NewLine + formatted; - } -} -#endif // WINDOWS diff --git a/MaddoShared/ImageCreatorImageSharp.cs b/MaddoShared/ImageCreatorImageSharp.cs index 1a7d009..008380c 100644 --- a/MaddoShared/ImageCreatorImageSharp.cs +++ b/MaddoShared/ImageCreatorImageSharp.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Threading.Tasks; -// System.Drawing not required for ImageSharp-based drawing in this class using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; @@ -17,11 +16,8 @@ using SixLabors.ImageSharp.Drawing; namespace MaddoShared; /// -/// 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. +/// Image creator implemented with SixLabors.ImageSharp for loading, EXIF orientation, +/// resizing, text/logo drawing, and saving. /// public class ImageCreatorImageSharp : IImageCreator { @@ -38,12 +34,11 @@ public class ImageCreatorImageSharp : IImageCreator { 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); + _logger.LogInformation("[ImageSharp] Processing {File} -> {Dest}", imgState.WorkFile?.FullName, imgState.DestDir?.FullName); using var fs = File.OpenRead(imgState.WorkFile.FullName); @@ -59,9 +54,6 @@ public class ImageCreatorImageSharp : IImageCreator // text to draw (horizontal vs vertical). ApplyExifOrientation(img, imgState); - // Determine output format - var forceJpg = _picSettings.UsaForzaJpg; - // Compute big size var bigSize = ComputeBigSize(img.Width, img.Height); @@ -71,10 +63,10 @@ public class ImageCreatorImageSharp : IImageCreator // Ensure destination exists imgState.DestDir?.Create(); - var fileNameBig = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileBig); + 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, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false); + await DrawAndSaveAsync(imgBig, fileNameBig, imgState, logoData, _picSettings.JpegQuality, isThumbnail: false).ConfigureAwait(false); // Create thumbnail if requested if (_picSettings.CreaMiniature) @@ -85,20 +77,16 @@ 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, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false); + await DrawAndSaveAsync(imgSmall, fileNameSmall, imgState, logoData, _picSettings.JpegQualityMin, isThumbnail: true).ConfigureAwait(false); } } catch (Exception ex) { - _logger.LogError(ex, "[Alternate] Error processing image {File}", imgState.WorkFile?.Name); + _logger.LogError(ex, "[ImageSharp] 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); @@ -110,7 +98,7 @@ public class ImageCreatorImageSharp : IImageCreator }; } - private async Task DrawAndSaveWithGdiAsync(Image imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail) + private async Task DrawAndSaveAsync(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. @@ -125,13 +113,13 @@ public class ImageCreatorImageSharp : IImageCreator } catch (Exception ex) { - _logger?.LogDebug(ex, "[Alternate] Failed to clear EXIF orientation on working image"); + _logger?.LogDebug(ex, "[ImageSharp] Failed to clear EXIF orientation on working image"); } // 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 + // Ensure thumbnail text is prepared before drawing. if (isThumbnail) { if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola)) @@ -285,8 +273,7 @@ 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). + // Draw logos only on full-size images. if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail) { try @@ -414,7 +401,7 @@ public class ImageCreatorImageSharp : IImageCreator } catch (Exception ex) { - _logger.LogError(ex, "[Alternate] Invalid transparent color setting {Color}", _picSettings.TransparentColor); + _logger.LogError(ex, "[ImageSharp] Invalid transparent color setting {Color}", _picSettings.TransparentColor); } } @@ -429,7 +416,7 @@ public class ImageCreatorImageSharp : IImageCreator } catch (Exception ex) { - _logger.LogError(ex, "[Alternate] Error drawing logo in ImageSharp pass"); + _logger.LogError(ex, "[ImageSharp] Error drawing logo"); } } @@ -444,8 +431,6 @@ public class ImageCreatorImageSharp : IImageCreator await working.SaveAsync(outStream, encoder).ConfigureAwait(false); } - // Removed GDI encoder helper; ImageSharp encoders are used instead. - private void PrepareVariablesMinimal(ImageState imgState) { imgState.NomeFileBig = imgState.WorkFile.Name; @@ -454,7 +439,6 @@ public class ImageCreatorImageSharp : IImageCreator 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 @@ -517,9 +501,7 @@ public class ImageCreatorImageSharp : IImageCreator 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) - // Set rotation flags on the state so other code can pick the correct - // text variant (vertical vs horizontal). Mirror ImageCreatorSharp logic. + // Set rotation flags on the state so other code can pick the correct text variant. imgState.FotoRuotaADestra = false; imgState.FotoRuotaASinistra = false; @@ -567,11 +549,11 @@ public class ImageCreatorImageSharp : IImageCreator catch (Exception ex) { // Non-fatal: log and continue - _logger?.LogDebug(ex, "[Alternate] Could not clear EXIF orientation tag"); + _logger?.LogDebug(ex, "[ImageSharp] Could not clear EXIF orientation tag"); } } - private System.Drawing.Size ComputeBigSize(int width, int height) + private Size ComputeBigSize(int width, int height) { // If original large size option requested, return original // otherwise compute based on width/height limits from settings @@ -580,16 +562,14 @@ public class ImageCreatorImageSharp : IImageCreator : CalculateThumbnailSize(width, height, _picSettings.AltezzaBig, "Altezza"); } - private System.Drawing.Size ComputeSmallSize(int width, int height) + private 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) + private static Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize) { double tempMultiplier; if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase)) @@ -601,7 +581,7 @@ public class ImageCreatorImageSharp : IImageCreator else tempMultiplier = maxPixel / (double)currentwidth; - var newSize = new System.Drawing.Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier)); + var newSize = new Size(Convert.ToInt32(currentwidth * tempMultiplier), Convert.ToInt32(currentheight * tempMultiplier)); return newSize; } diff --git a/MaddoShared/ImageCreatorMapper.cs b/MaddoShared/ImageCreatorMapper.cs deleted file mode 100644 index 4772b7a..0000000 --- a/MaddoShared/ImageCreatorMapper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -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 -{ - private readonly IServiceProvider _sp; - private readonly PicSettings _settings; - private readonly ILogger _logger; - - public ImageCreatorMapper(IServiceProvider sp, PicSettings settings, ILogger logger) - { - _sp = sp ?? throw new ArgumentNullException(nameof(sp)); - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - _logger = logger; - } - - 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, 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, byte[]? logoData) where T : IImageCreator - { - var impl = (IImageCreator?)_sp.GetService(typeof(T)); - if (impl is null) - { - _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, logoData); - } -} diff --git a/MaddoShared/ImageState.cs b/MaddoShared/ImageState.cs index 0090ecb..e80094c 100644 --- a/MaddoShared/ImageState.cs +++ b/MaddoShared/ImageState.cs @@ -1,5 +1,4 @@ using System; -using System.Drawing; using System.IO; namespace MaddoShared; @@ -28,18 +27,16 @@ public class ImageState public DateTime DataPartenzaI { get; set; } public string TestoOrario { get; set; } public string TestoFirmaPiccola { get; set; } - public Size ThumbSizeSmall { get; set; } - public Size ThumbSizeBig { get; set; } - public string NomeFileSmall{ get; set; } - public string NomeFileBig{ get; set; } - public string NomeFileBig2{ get; set; } + public string NomeFileSmall { get; set; } + public string NomeFileBig { get; set; } + public string NomeFileBig2 { get; set; } - public float YPosFromBottom{ get; set; } - public float YPosFromBottom1{ get; set; } - public float YPosFromBottom2{ get; set; } - public float YPosFromBottom3{ get; set; } - public float YPosFromBottom4{ get; set; } + public float YPosFromBottom { get; set; } + public float YPosFromBottom1 { get; set; } + public float YPosFromBottom2 { get; set; } + public float YPosFromBottom3 { get; set; } + public float YPosFromBottom4 { get; set; } - public Orientations Orientation{ get; set; } - public DateTime? CreationDate{ get; set; } + public Orientations Orientation { get; set; } + public DateTime? CreationDate { get; set; } } \ No newline at end of file diff --git a/MaddoShared/MaddoShared.csproj b/MaddoShared/MaddoShared.csproj index a0cfd03..6d0ee05 100644 --- a/MaddoShared/MaddoShared.csproj +++ b/MaddoShared/MaddoShared.csproj @@ -1,15 +1,11 @@  - net10.0;net10.0-windows + net10.0 Library enable enable false x64 - true - false - - $(DefineConstants);WINDOWS @@ -33,6 +29,5 @@ all - \ No newline at end of file diff --git a/MaddoShared/PicSettings.cs b/MaddoShared/PicSettings.cs index b633db3..0623a7f 100644 --- a/MaddoShared/PicSettings.cs +++ b/MaddoShared/PicSettings.cs @@ -1,6 +1,6 @@ using System; -using System.Drawing; using System.IO; +using SixLabors.ImageSharp.PixelFormats; namespace MaddoShared; @@ -35,7 +35,7 @@ public class PicSettings public int Margine { get; set; } public int LogoAltezza { get; set; } public int LogoLarghezza { get; set; } - public Color FontColoreRGB { get; set; } + public Rgba32 FontColoreRGB { get; set; } = new(255, 255, 0, 255); public bool LogoAggiungi { get; set; } // Initialize logo-related strings to safe defaults to avoid null reference issues public string LogoNomeFile { get; set; } = string.Empty; @@ -81,6 +81,4 @@ public class PicSettings public bool FotoRuotaASinistra { get; set; } = false; public string TempMinText { get; set; } = string.Empty; public bool OverwriteFiles { get; set; } = false; - // Which image creator to use: "Sharp" for current implementation, "Alternate" for alternate library - public string ImageCreatorProvider { get; set; } = "Sharp"; } \ No newline at end of file diff --git a/README.md b/README.md index 1d4631a..dd8716f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Catalog 3 The build embeds an expiration date from the `CatalogLiteExpirationDate` MSBuild property: ```powershell -dotnet publish CatalogLite/CatalogLite.csproj -c Release -r win-x64 --self-contained true -p:CatalogLiteExpirationDate=2026-12-31 +dotnet publish CatalogLite/CatalogLite.csproj -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true -p:CatalogLiteExpirationDate=2026-12-31 ``` The separate Forgejo workflow is `.forgejo/workflows/build-catalog-lite.yml`; run it manually and set `expiration_date` in `yyyy-MM-dd` format. diff --git a/docs/image-generation-tests-plan.md b/docs/image-generation-tests-plan.md index dcddcd8..2745450 100644 --- a/docs/image-generation-tests-plan.md +++ b/docs/image-generation-tests-plan.md @@ -51,7 +51,7 @@ Verification approach Scope boundaries ---------------- - In scope: `ImageCreatorImageSharp` behavior: resize, EXIF rotation, text presence/position, logo position/opactiy, thumbnails. -- Out of scope: `ImageCreatorGDI` (excluded), OCR verification of exact text glyphs, font-subpixel metrics, performance testing. +- Out of scope: OCR verification of exact text glyphs, font-subpixel metrics, performance testing. Implementation notes -------------------- diff --git a/imagecatalog/AvaloniaViews/GeneralTabView.axaml b/imagecatalog/AvaloniaViews/GeneralTabView.axaml index ed0dd98..082d502 100644 --- a/imagecatalog/AvaloniaViews/GeneralTabView.axaml +++ b/imagecatalog/AvaloniaViews/GeneralTabView.axaml @@ -38,12 +38,6 @@ - - - - - - diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index d4a3dc4..4ed641a 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -6,12 +6,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -#if WINDOWS -using System.Drawing.Text; -#endif using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Globalization; using System.Threading; @@ -23,6 +19,7 @@ using AutoMapper; using MaddoShared; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; +using SixLabors.Fonts; namespace ImageCatalog_2 { @@ -561,16 +558,19 @@ namespace ImageCatalog_2 private List LoadAvailableFonts() { -#if WINDOWS - var fonts = new List(); - using (var installedFonts = new InstalledFontCollection()) + try { - fonts.AddRange(installedFonts.Families.Select(f => f.Name)); + return SystemFonts.Collection.Families + .Select(f => f.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(name => name, StringComparer.CurrentCultureIgnoreCase) + .ToList(); + } + catch + { + return new List(); } - return fonts; -#else - return new List(); -#endif } private CancellationTokenSource? _mainToken; @@ -841,56 +841,6 @@ namespace ImageCatalog_2 set => _visual.LogoTransparency = value; } - // Image library selection (UI radio buttons bind to the boolean helpers) - 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". - /// 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)); - NotifyPropertyChanged(nameof(IsRunningOnWindows)); - } - } - - 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/ExifReader.cs b/imagecatalog/ExifReader.cs deleted file mode 100644 index 97005ca..0000000 --- a/imagecatalog/ExifReader.cs +++ /dev/null @@ -1,1190 +0,0 @@ -/// ----------------------------------------------------------------------------- -/// -/// Utility class for reading EXIF data from images. Provides abstraction -/// for most common data and generic utilities for work with all other. -/// -/// -/// Copyright (c) Michal A. Valášek - Altair Communications, 2003 -/// Copmany: http://software.altaircom.net * support@altaircom.net -/// Private: http://www.rider.cz * developer@rider.cz -/// This is free software licensed under GNU Lesser General Public License -/// -/// -/// [altair] 10.9.2003 Created -/// -/// ----------------------------------------------------------------------------- -using System; -using System.Drawing; -using Microsoft.VisualBasic; -using Microsoft.VisualBasic.CompilerServices; - -namespace ImageCatalog -{ - public class ExifReader : IDisposable - { - private Bitmap Image; - - /// ----------------------------------------------------------------------------- - /// - /// Contains possible values of EXIF tag names (ID) - /// - /// See GdiPlusImaging.h - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum TagNames - { - ExifIFD = 0x8769, - GpsIFD = 0x8825, - NewSubfileType = 0xFE, - SubfileType = 0xFF, - ImageWidth = 0x100, - ImageHeight = 0x101, - BitsPerSample = 0x102, - Compression = 0x103, - PhotometricInterp = 0x106, - ThreshHolding = 0x107, - CellWidth = 0x108, - CellHeight = 0x109, - FillOrder = 0x10A, - DocumentName = 0x10D, - ImageDescription = 0x10E, - EquipMake = 0x10F, - EquipModel = 0x110, - StripOffsets = 0x111, - Orientation = 0x112, - SamplesPerPixel = 0x115, - RowsPerStrip = 0x116, - StripBytesCount = 0x117, - MinSampleValue = 0x118, - MaxSampleValue = 0x119, - XResolution = 0x11A, - YResolution = 0x11B, - PlanarConfig = 0x11C, - PageName = 0x11D, - XPosition = 0x11E, - YPosition = 0x11F, - FreeOffset = 0x120, - FreeByteCounts = 0x121, - GrayResponseUnit = 0x122, - GrayResponseCurve = 0x123, - T4Option = 0x124, - T6Option = 0x125, - ResolutionUnit = 0x128, - PageNumber = 0x129, - TransferFuncition = 0x12D, - SoftwareUsed = 0x131, - DateTime = 0x132, - Artist = 0x13B, - HostComputer = 0x13C, - Predictor = 0x13D, - WhitePoint = 0x13E, - PrimaryChromaticities = 0x13F, - ColorMap = 0x140, - HalftoneHints = 0x141, - TileWidth = 0x142, - TileLength = 0x143, - TileOffset = 0x144, - TileByteCounts = 0x145, - InkSet = 0x14C, - InkNames = 0x14D, - NumberOfInks = 0x14E, - DotRange = 0x150, - TargetPrinter = 0x151, - ExtraSamples = 0x152, - SampleFormat = 0x153, - SMinSampleValue = 0x154, - SMaxSampleValue = 0x155, - TransferRange = 0x156, - JPEGProc = 0x200, - JPEGInterFormat = 0x201, - JPEGInterLength = 0x202, - JPEGRestartInterval = 0x203, - JPEGLosslessPredictors = 0x205, - JPEGPointTransforms = 0x206, - JPEGQTables = 0x207, - JPEGDCTables = 0x208, - JPEGACTables = 0x209, - YCbCrCoefficients = 0x211, - YCbCrSubsampling = 0x212, - YCbCrPositioning = 0x213, - REFBlackWhite = 0x214, - ICCProfile = 0x8773, - Gamma = 0x301, - ICCProfileDescriptor = 0x302, - SRGBRenderingIntent = 0x303, - ImageTitle = 0x320, - Copyright = 0x8298, - ResolutionXUnit = 0x5001, - ResolutionYUnit = 0x5002, - ResolutionXLengthUnit = 0x5003, - ResolutionYLengthUnit = 0x5004, - PrintFlags = 0x5005, - PrintFlagsVersion = 0x5006, - PrintFlagsCrop = 0x5007, - PrintFlagsBleedWidth = 0x5008, - PrintFlagsBleedWidthScale = 0x5009, - HalftoneLPI = 0x500A, - HalftoneLPIUnit = 0x500B, - HalftoneDegree = 0x500C, - HalftoneShape = 0x500D, - HalftoneMisc = 0x500E, - HalftoneScreen = 0x500F, - JPEGQuality = 0x5010, - GridSize = 0x5011, - ThumbnailFormat = 0x5012, - ThumbnailWidth = 0x5013, - ThumbnailHeight = 0x5014, - ThumbnailColorDepth = 0x5015, - ThumbnailPlanes = 0x5016, - ThumbnailRawBytes = 0x5017, - ThumbnailSize = 0x5018, - ThumbnailCompressedSize = 0x5019, - ColorTransferFunction = 0x501A, - ThumbnailData = 0x501B, - ThumbnailImageWidth = 0x5020, - ThumbnailImageHeight = 0x502, - ThumbnailBitsPerSample = 0x5022, - ThumbnailCompression = 0x5023, - ThumbnailPhotometricInterp = 0x5024, - ThumbnailImageDescription = 0x5025, - ThumbnailEquipMake = 0x5026, - ThumbnailEquipModel = 0x5027, - ThumbnailStripOffsets = 0x5028, - ThumbnailOrientation = 0x5029, - ThumbnailSamplesPerPixel = 0x502A, - ThumbnailRowsPerStrip = 0x502B, - ThumbnailStripBytesCount = 0x502C, - ThumbnailResolutionX = 0x502D, - ThumbnailResolutionY = 0x502E, - ThumbnailPlanarConfig = 0x502F, - ThumbnailResolutionUnit = 0x5030, - ThumbnailTransferFunction = 0x5031, - ThumbnailSoftwareUsed = 0x5032, - ThumbnailDateTime = 0x5033, - ThumbnailArtist = 0x5034, - ThumbnailWhitePoint = 0x5035, - ThumbnailPrimaryChromaticities = 0x5036, - ThumbnailYCbCrCoefficients = 0x5037, - ThumbnailYCbCrSubsampling = 0x5038, - ThumbnailYCbCrPositioning = 0x5039, - ThumbnailRefBlackWhite = 0x503A, - ThumbnailCopyRight = 0x503B, - LuminanceTable = 0x5090, - ChrominanceTable = 0x5091, - FrameDelay = 0x5100, - LoopCount = 0x5101, - PixelUnit = 0x5110, - PixelPerUnitX = 0x5111, - PixelPerUnitY = 0x5112, - PaletteHistogram = 0x5113, - ExifExposureTime = 0x829A, - ExifFNumber = 0x829D, - ExifExposureProg = 0x8822, - ExifSpectralSense = 0x8824, - ExifISOSpeed = 0x8827, - ExifOECF = 0x8828, - ExifVer = 0x9000, - ExifDTOrig = 0x9003, - ExifDTDigitized = 0x9004, - ExifCompConfig = 0x9101, - ExifCompBPP = 0x9102, - ExifShutterSpeed = 0x9201, - ExifAperture = 0x9202, - ExifBrightness = 0x9203, - ExifExposureBias = 0x9204, - ExifMaxAperture = 0x9205, - ExifSubjectDist = 0x9206, - ExifMeteringMode = 0x9207, - ExifLightSource = 0x9208, - ExifFlash = 0x9209, - ExifFocalLength = 0x920A, - ExifMakerNote = 0x927C, - ExifUserComment = 0x9286, - ExifDTSubsec = 0x9290, - ExifDTOrigSS = 0x9291, - ExifDTDigSS = 0x9292, - ExifFPXVer = 0xA000, - ExifColorSpace = 0xA001, - ExifPixXDim = 0xA002, - ExifPixYDim = 0xA003, - ExifRelatedWav = 0xA004, - ExifInterop = 0xA005, - ExifFlashEnergy = 0xA20B, - ExifSpatialFR = 0xA20C, - ExifFocalXRes = 0xA20E, - ExifFocalYRes = 0xA20F, - ExifFocalResUnit = 0xA210, - ExifSubjectLoc = 0xA214, - ExifExposureIndex = 0xA215, - ExifSensingMethod = 0xA217, - ExifFileSource = 0xA300, - ExifSceneType = 0xA301, - ExifCfaPattern = 0xA302, - GpsVer = 0x0, - GpsLatitudeRef = 0x1, - GpsLatitude = 0x2, - GpsLongitudeRef = 0x3, - GpsLongitude = 0x4, - GpsAltitudeRef = 0x5, - GpsAltitude = 0x6, - GpsGpsTime = 0x7, - GpsGpsSatellites = 0x8, - GpsGpsStatus = 0x9, - GpsGpsMeasureMode = 0xA, - GpsGpsDop = 0xB, - GpsSpeedRef = 0xC, - GpsSpeed = 0xD, - GpsTrackRef = 0xE, - GpsTrack = 0xF, - GpsImgDirRef = 0x10, - GpsImgDir = 0x11, - GpsMapDatum = 0x12, - GpsDestLatRef = 0x13, - GpsDestLat = 0x14, - GpsDestLongRef = 0x15, - GpsDestLong = 0x16, - GpsDestBearRef = 0x17, - GpsDestBear = 0x18, - GpsDestDistRef = 0x19, - GpsDestDist = 0x1A - } - - /// ----------------------------------------------------------------------------- - /// - /// Real position of 0th row and column of picture - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum Orientations - { - TopLeft = 1, - TopRight = 2, - BottomRight = 3, - BottomLeft = 4, - LeftTop = 5, - RightTop = 6, - RightBottom = 7, - LftBottom = 8 - } - - /// ----------------------------------------------------------------------------- - /// - /// Exposure programs - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum ExposurePrograms - { - Manual = 1, - Normal = 2, - AperturePriority = 3, - ShutterPriority = 4, - Creative = 5, - Action = 6, - Portrait = 7, - Landscape = 8 - } - - /// ----------------------------------------------------------------------------- - /// - /// Exposure metering modes - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum ExposureMeteringModes - { - Unknown = 0, - Average = 1, - CenterWeightedAverage = 2, - Spot = 3, - MultiSpot = 4, - MultiSegment = 5, - Partial = 6, - Other = 255 - } - - /// ----------------------------------------------------------------------------- - /// - /// Flash activity modes - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum FlashModes - { - NotFired = 0, - Fired = 1, - FiredButNoStrobeReturned = 5, - FiredAndStrobeReturned = 7 - } - - /// ----------------------------------------------------------------------------- - /// - /// Possible light sources (white balance) - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public enum LightSources - { - Unknown = 0, - Daylight = 1, - Fluorescent = 2, - Tungsten = 3, - Flash = 10, - StandardLightA = 17, - StandardLightB = 18, - StandardLightC = 19, - D55 = 20, - D65 = 21, - D75 = 22, - Other = 255 - } - - /// ----------------------------------------------------------------------------- - /// - /// Represents rational which is type of some Exif properties - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public struct Rational - { - public int Numerator; - public int Denominator; - - /// ----------------------------------------------------------------------------- - /// - /// Converts rational to string representation - /// - /// Optional, default "/". String to be used as delimiter of components. - /// String representation of the rational. - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public new string ToString(string Delimiter = "/") - { - return Numerator + Delimiter + Denominator; - } - - /// ----------------------------------------------------------------------------- - /// - /// Converts rational to double precision real number - /// - /// The rational as double precision real number. - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double ToDouble() - { - return Numerator / (double)Denominator; - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Initializes new instance of this class. - /// - /// Bitmap to read exif information from - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public ExifReader(Bitmap Bitmap) - { - if (Bitmap is null) - throw new ArgumentNullException("Bitmap"); - Image = Bitmap; - } - - /// ----------------------------------------------------------------------------- - /// - /// Returns all available data in formatted string form - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public override string ToString() - { - var SB = new System.Text.StringBuilder(); - SB.Append("Image:"); - SB.Append(@"\n\tDimensions: " + Width + " x " + Height + " px"); - SB.Append(@"\n\tResolution: " + ResolutionX + " x " + ResolutionY + " dpi"); - SB.Append(@"\n\tOrientation: " + Enum.GetName(typeof(Orientations), Orientation)); - SB.Append(@"\n\tTitle: " + Title); - SB.Append(@"\n\tDescription: " + Description); - SB.Append(@"\n\tCopyright: " + Copyright); - SB.Append(@"\nEquipment:"); - SB.Append(@"\n\tMaker: " + EquipmentMaker); - SB.Append(@"\n\tModel: " + EquipmentModel); - SB.Append(@"\n\tSoftware: " + Software); - SB.Append(@"\nDate and time:"); - SB.Append(@"\n\tGeneral: " + DateTimeLastModified.ToString()); - SB.Append(@"\n\tOriginal: " + DateTimeOriginal.ToString()); - SB.Append(@"\n\tDigitized: " + DateTimeDigitized.ToString()); - SB.Append(@"\nShooting conditions:"); - SB.Append(@"\n\tExposure time: " + ExposureTime.ToString("N4") + " s"); - SB.Append(@"\n\tExposure program: " + Enum.GetName(typeof(ExposurePrograms), ExposureProgram)); - SB.Append(@"\n\tExposure mode: " + Enum.GetName(typeof(ExposureMeteringModes), ExposureMeteringMode)); - SB.Append(@"\n\tAperture: F" + Aperture.ToString("N2")); - SB.Append(@"\n\tISO sensitivity: " + ISO); - SB.Append(@"\n\tSubject distance: " + SubjectDistance.ToString("N2") + " m"); - SB.Append(@"\n\tFocal length: " + FocalLength); - SB.Append(@"\n\tFlash: " + Enum.GetName(typeof(FlashModes), FlashMode)); - SB.Append(@"\n\tLight source (WB): " + Enum.GetName(typeof(LightSources), LightSource)); - SB.Append(@"\n\nCopyright (c) Michal A. Valasek - Altair Communications, 2003"); - SB.Append(@"\nhttp://software.altaircom.net * support@altaircom.net"); - SB.Replace(@"\n", Constants.vbCrLf); - SB.Replace(@"\t", Constants.vbTab); - return SB.ToString(); - } - - /// ----------------------------------------------------------------------------- - /// - /// Brand of equipment (EXIF EquipMake) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string EquipmentMaker - { - get - { - return GetPropertyString((int)TagNames.EquipMake); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Model of equipment (EXIF EquipModel) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string EquipmentModel - { - get - { - return GetPropertyString((int)TagNames.EquipModel); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Software used for processing (EXIF Software) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string Software - { - get - { - return GetPropertyString((int)TagNames.SoftwareUsed); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Orientation of image (position of row 0, column 0) (EXIF Orientation) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public Orientations Orientation - { - get - { - int X = GetPropertyInt16((int)TagNames.Orientation); - if (!Enum.IsDefined(typeof(Orientations), X)) - { - return Orientations.TopLeft; - } - else - { - return (Orientations)Conversions.ToInteger(Enum.Parse(typeof(Orientations), Enum.GetName(typeof(Orientations), X))); - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Time when image was last modified (EXIF DateTime). - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public DateTime DateTimeLastModified - { - get - { - try - { - return DateTime.ParseExact(GetPropertyString((int)TagNames.DateTime), @"yyyy\:MM\:dd HH\:mm\:ss", null); - } - catch (Exception ex) - { - return DateTime.MinValue; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Time when image was taken (EXIF DateTimeOriginal). - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public DateTime DateTimeOriginal - { - get - { - try - { - return DateTime.ParseExact(GetPropertyString((int)TagNames.ExifDTOrig), @"yyyy\:MM\:dd HH\:mm\:ss", null); - } - catch (Exception ex) - { - return DateTime.MinValue; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Time when image was digitized (EXIF DateTimeDigitized). - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public DateTime DateTimeDigitized - { - get - { - try - { - return DateTime.ParseExact(GetPropertyString((int)TagNames.ExifDTDigitized), @"yyyy\:MM\:dd HH\:mm\:ss", null); - } - catch (Exception ex) - { - return DateTime.MinValue; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Image width - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public short Width - { - get - { - return GetPropertyInt16((int)TagNames.ImageWidth); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Image height - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public short Height - { - get - { - return GetPropertyInt16((int)TagNames.ImageHeight); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// X resolution in dpi (EXIF XResolution/ResolutionUnit) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double ResolutionX - { - get - { - double R = GetPropertyRational((int)TagNames.XResolution).ToDouble(); - if (GetPropertyInt16((int)TagNames.ResolutionUnit) == 3) - { - // -- resolution is in points/cm - return R * 2.54d; - } - else - { - // -- resolution is in points/inch - return R; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Y resolution in dpi (EXIF YResolution/ResolutionUnit) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double ResolutionY - { - get - { - double R = GetPropertyRational((int)TagNames.YResolution).ToDouble(); - if (GetPropertyInt16((int)TagNames.ResolutionUnit) == 3) - { - // -- resolution is in points/cm - return R * 2.54d; - } - else - { - // -- resolution is in points/inch - return R; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Image title (EXIF ImageTitle) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string Title - { - get - { - return GetPropertyString((int)TagNames.ImageTitle); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Image description (EXIF ImageDescription) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string Description - { - get - { - return GetPropertyString((int)TagNames.ImageDescription); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Image copyright (EXIF Copyright) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string Copyright - { - get - { - return GetPropertyString((int)TagNames.Copyright); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Exposure time in seconds (EXIF ExifExposureTime/ExifShutterSpeed) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double ExposureTime - { - get - { - if (IsPropertyDefined((int)TagNames.ExifExposureTime)) - { - // -- Exposure time is explicitly specified - return GetPropertyRational((int)TagNames.ExifExposureTime).ToDouble(); - } - else if (IsPropertyDefined((int)TagNames.ExifShutterSpeed)) - { - // -- Compute exposure time from shutter speed - return 1d / Math.Pow(2d, GetPropertyRational((int)TagNames.ExifShutterSpeed).ToDouble()); - } - else - { - // -- Can't figure out - return 0d; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Aperture value as F number (EXIF ExifFNumber/ExifApertureValue) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double Aperture - { - get - { - if (IsPropertyDefined((int)TagNames.ExifFNumber)) - { - return GetPropertyRational((int)TagNames.ExifFNumber).ToDouble(); - } - else if (IsPropertyDefined((int)TagNames.ExifAperture)) - { - return Math.Pow(Math.Sqrt(2d), GetPropertyRational((int)TagNames.ExifAperture).ToDouble()); - } - else - { - return 0d; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Exposure program used (EXIF ExifExposureProg) - /// - /// - /// If not specified, returns Normal (2) - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public ExposurePrograms ExposureProgram - { - get - { - int X = GetPropertyInt16((int)TagNames.ExifExposureProg); - if (Enum.IsDefined(typeof(ExposurePrograms), X)) - { - return (ExposurePrograms)Conversions.ToInteger(Enum.Parse(typeof(ExposurePrograms), Enum.GetName(typeof(ExposurePrograms), X))); - } - else - { - return ExposurePrograms.Normal; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// ISO sensitivity - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public short ISO - { - get - { - return GetPropertyInt16((int)TagNames.ExifISOSpeed); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Subject distance in meters (EXIF SubjectDistance) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double SubjectDistance - { - get - { - return GetPropertyRational((int)TagNames.ExifSubjectDist).ToDouble(); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Exposure method metering mode used (EXIF MeteringMode) - /// - /// - /// If not specified, returns Unknown (0) - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public ExposureMeteringModes ExposureMeteringMode - { - get - { - int X = GetPropertyInt16((int)TagNames.ExifMeteringMode); - if (Enum.IsDefined(typeof(ExposureMeteringModes), X)) - { - return (ExposureMeteringModes)Conversions.ToInteger(Enum.Parse(typeof(ExposureMeteringModes), Enum.GetName(typeof(ExposureMeteringModes), X))); - } - else - { - return ExposureMeteringModes.Unknown; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Focal length of lenses in mm (EXIF FocalLength) - /// - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public double FocalLength - { - get - { - return GetPropertyRational((int)TagNames.ExifFocalLength).ToDouble(); - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Flash mode (EXIF Flash) - /// - /// - /// If not present, value NotFired (0) is returned - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public FlashModes FlashMode - { - get - { - int X = GetPropertyInt16((int)TagNames.ExifFlash); - if (Enum.IsDefined(typeof(FlashModes), X)) - { - return (FlashModes)Conversions.ToInteger(Enum.Parse(typeof(FlashModes), Enum.GetName(typeof(FlashModes), X))); - } - else - { - return FlashModes.NotFired; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Light source / white balance (EXIF LightSource) - /// - /// - /// If not specified, returns Unknown (0). - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public LightSources LightSource - { - get - { - int X = GetPropertyInt16((int)TagNames.ExifLightSource); - if (Enum.IsDefined(typeof(LightSources), X)) - { - return (LightSources)Conversions.ToInteger(Enum.Parse(typeof(LightSources), Enum.GetName(typeof(LightSources), X))); - } - else - { - return LightSources.Unknown; - } - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Checks if current image has specified certain property - /// - /// - /// True if image has specified property, False otherwise. - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public bool IsPropertyDefined(int PID) - { - return Array.IndexOf(Image.PropertyIdList, PID) > -1; - } - - /// ----------------------------------------------------------------------------- - /// - /// Gets specified Int32 property - /// - /// Property ID - /// Optional, default 0. Default value returned if property is not present. - /// Value of property or DefaultValue if property is not present. - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public int GetPropertyInt32(int PID, int DefaultValue = 0) - { - if (IsPropertyDefined(PID)) - { - return GetInt32(Image.GetPropertyItem(PID).Value); - } - else - { - return DefaultValue; - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Gets specified Int16 property - /// - /// Property ID - /// Optional, default 0. Default value returned if property is not present. - /// Value of property or DefaultValue if property is not present. - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public short GetPropertyInt16(int PID, short DefaultValue = 0) - { - if (IsPropertyDefined(PID)) - { - return GetInt16(Image.GetPropertyItem(PID).Value); - } - else - { - return DefaultValue; - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Gets specified string property - /// - /// Property ID - /// Optional, default String.Empty. Default value returned if property is not present. - /// - /// Value of property or DefaultValue if property is not present. - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public string GetPropertyString(int PID, string DefaultValue = "") - { - if (IsPropertyDefined(PID)) - { - return GetString(Image.GetPropertyItem(PID).Value); - } - else - { - return DefaultValue; - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Gets specified rational property - /// - /// Property ID - /// - /// Value of property or 0/1 if not present. - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public Rational GetPropertyRational(int PID) - { - if (IsPropertyDefined(PID)) - { - return GetRational(Image.GetPropertyItem(PID).Value); - } - else - { - Rational R; - R.Numerator = 0; - R.Denominator = 1; - return R; - } - } - - /// ----------------------------------------------------------------------------- - /// - /// Reads Int32 from EXIF bytearray. - /// - /// EXIF bytearray to process - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public static int GetInt32(byte[] B) - { - if (B.Length < 4) - throw new ArgumentException("Data too short (4 bytes expected)", "B"); - return B[3] << 24 | B[2] << 16 | B[1] << 8 | B[0]; - } - - /// ----------------------------------------------------------------------------- - /// - /// Reads Int16 from EXIF bytearray. - /// - /// EXIF bytearray to process - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public static short GetInt16(byte[] B) - { - if (B.Length < 2) - throw new ArgumentException("Data too short (2 bytes expected)", "B"); - return (short)(B[1] << 8 | B[0]); - } - - /// ----------------------------------------------------------------------------- - /// - /// Reads string from EXIF bytearray. - /// - /// EXIF bytearray to process - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public static string GetString(byte[] B) - { - string R = System.Text.Encoding.ASCII.GetString(B); - if (R.EndsWith(Constants.vbNullChar)) - R = R.Substring(0, R.Length - 1); - return R; - } - - /// ----------------------------------------------------------------------------- - /// - /// Reads rational from EXIF bytearray. - /// - /// EXIF bytearray to process - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public static Rational GetRational(byte[] B) - { - var R = new Rational(); - byte[] N = new byte[4], D = new byte[4]; - Array.Copy(B, 0, N, 0, 4); - Array.Copy(B, 4, D, 0, 4); - R.Denominator = GetInt32(D); - R.Numerator = GetInt32(N); - return R; - } - - /// ----------------------------------------------------------------------------- - /// - /// Disposes unmanaged resources of this class - /// - /// - /// - /// [altair] 10.9.2003 Created - /// - /// ----------------------------------------------------------------------------- - public void Dispose() - { - Image.Dispose(); - } - } -} \ No newline at end of file diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 11eda12..15fbc3b 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -72,6 +72,7 @@ + diff --git a/imagecatalog/Mappings/DataModelMappingProfile.cs b/imagecatalog/Mappings/DataModelMappingProfile.cs index 2706a88..8ed1859 100644 --- a/imagecatalog/Mappings/DataModelMappingProfile.cs +++ b/imagecatalog/Mappings/DataModelMappingProfile.cs @@ -1,6 +1,7 @@ -using System.Drawing; -using AutoMapper; +using AutoMapper; using MaddoShared; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace ImageCatalog_2.Mappings; @@ -15,7 +16,7 @@ public class DataModelMappingProfile : Profile // Paths .ForMember(dest => dest.DirectorySorgente, opt => opt.MapFrom(src => src.SourcePath)) .ForMember(dest => dest.DirectoryDestinazione, opt => opt.MapFrom(src => src.DestinationPath)) - + // Font and text settings .ForMember(dest => dest.DimStandard, opt => opt.MapFrom(src => src.FontSize)) .ForMember(dest => dest.DimStandardMiniatura, opt => opt.MapFrom(src => src.FontSizeThumbnail)) @@ -25,8 +26,8 @@ public class DataModelMappingProfile : Profile .ForMember(dest => dest.Allineamento, opt => opt.MapFrom(src => src.HorizontalAlignment)) .ForMember(dest => dest.Trasparenza, opt => opt.MapFrom(src => src.TextTransparency)) .ForMember(dest => dest.Margine, opt => opt.MapFrom(src => src.TextMargin)) - .ForMember(dest => dest.FontColoreRGB, opt => opt.MapFrom(src => ColorTranslator.FromHtml(src.TextColorRGB))) - + .ForMember(dest => dest.FontColoreRGB, opt => opt.MapFrom(src => ParseColor(src.TextColorRGB))) + // Thumbnail settings .ForMember(dest => dest.AltezzaSmall, opt => opt.MapFrom(src => src.ThumbnailHeight)) .ForMember(dest => dest.LarghezzaSmall, opt => opt.MapFrom(src => src.ThumbnailWidth)) @@ -34,14 +35,14 @@ public class DataModelMappingProfile : Profile .ForMember(dest => dest.CreaMiniature, opt => opt.MapFrom(src => src.CreateThumbnails)) .ForMember(dest => dest.JpegQualityMin, opt => opt.MapFrom(src => src.JpegQualityThumbnail)) .ForMember(dest => dest.DimMin, opt => opt.MapFrom(src => src.FontSizeThumbnail)) - + // Big photo settings .ForMember(dest => dest.AltezzaBig, opt => opt.MapFrom(src => src.PhotoBigHeight)) .ForMember(dest => dest.LarghezzaBig, opt => opt.MapFrom(src => src.PhotoBigWidth)) .ForMember(dest => dest.FotoGrandeDimOrigina, opt => opt.MapFrom(src => src.KeepOriginalDimensions)) .ForMember(dest => dest.JpegQuality, opt => opt.MapFrom(src => src.JpegQuality)) .ForMember(dest => dest.Codice, opt => opt.MapFrom(src => src.BigPhotoSuffix)) - + // Logo settings .ForMember(dest => dest.LogoAggiungi, opt => opt.MapFrom(src => src.AddLogo)) .ForMember(dest => dest.LogoNomeFile, opt => opt.MapFrom(src => src.LogoFile)) @@ -51,15 +52,15 @@ public class DataModelMappingProfile : Profile .ForMember(dest => dest.LogoTrasparenza, opt => opt.MapFrom(src => src.LogoTransparency.ToString())) .ForMember(dest => dest.LogoPosizioneH, opt => opt.MapFrom(src => src.LogoHorizontalPosition)) .ForMember(dest => dest.LogoPosizioneV, opt => opt.MapFrom(src => src.LogoVerticalPosition)) - + // Text content .ForMember(dest => dest.TestoFirmaStart, opt => opt.MapFrom(src => src.HorizontalText)) .ForMember(dest => dest.TestoFirmaStartV, opt => opt.MapFrom(src => src.VerticalText)) - + // Vertical text settings .ForMember(dest => dest.DimVert, opt => opt.MapFrom(src => src.VerticalTextSize)) .ForMember(dest => dest.MargVert, opt => opt.MapFrom(src => src.VerticalTextMargin)) - + // Boolean flags .ForMember(dest => dest.UsaRotazioneAutomatica, opt => opt.MapFrom(src => src.AutomaticRotation)) .ForMember(dest => dest.UsaForzaJpg, opt => opt.MapFrom(src => src.ForceJpeg)) @@ -68,18 +69,18 @@ public class DataModelMappingProfile : Profile .ForMember(dest => dest.UsaOrarioTestoApplicare, opt => opt.MapFrom(src => src.AddTime)) .ForMember(dest => dest.UsaTempoGaraTestoApplicare, opt => opt.MapFrom(src => src.AddRaceTime)) .ForMember(dest => dest.OverwriteFiles, opt => opt.MapFrom(src => src.OverwriteImages)) - + // Additional settings .ForMember(dest => dest.UsaOrarioMiniatura, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.Time)) .ForMember(dest => dest.DataPartenza, opt => opt.MapFrom(src => src.RaceStartDate)) .ForMember(dest => dest.TestoOrario, opt => opt.MapFrom(src => src.TimeLabel)) .ForMember(dest => dest.TestoMin, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.FileName)) - + // Thumbnail text options .ForMember(dest => dest.AggiungiScritteMiniature, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.Text)) .ForMember(dest => dest.AggTempoGaraMin, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.RaceTime)) .ForMember(dest => dest.AggNumTempMin, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.FileNameAndTime)) - + // Ignore unmapped properties .ForMember(dest => dest.DestDir, opt => opt.Ignore()) .ForMember(dest => dest.SecretDefault, opt => opt.Ignore()) @@ -91,4 +92,27 @@ public class DataModelMappingProfile : Profile .ForMember(dest => dest.FotoRuotaASinistra, opt => opt.Ignore()) .ForMember(dest => dest.TempMinText, opt => opt.Ignore()); } + + private static Rgba32 ParseColor(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new Rgba32(255, 255, 0, 255); + } + + try + { + var normalized = value.Trim(); + if (normalized.Length == 6 && normalized.All(Uri.IsHexDigit)) + { + normalized = "#" + normalized; + } + + return Color.Parse(normalized).ToPixel(); + } + catch + { + return new Rgba32(255, 255, 0, 255); + } + } } diff --git a/imagecatalog/Models/SettingsDto.cs b/imagecatalog/Models/SettingsDto.cs index fd68763..41c7dd5 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -192,11 +192,6 @@ namespace ImageCatalog_2.Models [XmlElement("UsaColoreTrasparente")] public bool UseTransparentColor { get; set; } = false; - // Selected image processing library (e.g., "System.Graphics" or "ImageSharp") - [JsonPropertyName("ImageLibrary")] - [XmlElement("ImageLibrary")] - public string ImageLibrary { get; set; } = "ImageSharp"; - // Options [JsonPropertyName("ForceJpeg")] [XmlElement("GeneraleForzaJpg")] diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs index 798a03d..aa02e40 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -143,13 +143,7 @@ static class Program services.AddTransient(); services.AddTransient(); services.AddTransient(); -#if WINDOWS - services.AddTransient(); -#endif - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(sp => sp.GetRequiredService()); + services.AddTransient(); var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ImageCatalog", "userprefs.xml");