This commit is contained in:
parent
ddf47ad51b
commit
d76e133f18
31 changed files with 236 additions and 2592 deletions
|
|
@ -91,6 +91,13 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: Upload publish artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: Upload publish artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
10
.github/copilot-instructions.md
vendored
10
.github/copilot-instructions.md
vendored
|
|
@ -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)
|
1. User configures paths/settings in the UI (`DataModel.cs` — MVVM ViewModel)
|
||||||
2. `ProcessImagesCommand` triggers `ImageCreationService`
|
2. `ProcessImagesCommand` triggers `ImageCreationService`
|
||||||
3. `ImageCreationService` processes files in parallel chunks, with configurable concurrency and batch size (GC flush between chunks)
|
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
|
5. Output: resized/watermarked/overlaid images written to a destination folder hierarchy
|
||||||
|
|
||||||
### Key Abstractions (MaddoShared)
|
### 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
|
- **`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)
|
- **`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
|
- **`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
|
## Conventions
|
||||||
|
|
||||||
### C# Style
|
### C# Style
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,13 @@ build_windows:
|
||||||
# Produce a single-file, ready-to-run publish so downstream jobs only need the EXE.
|
# Produce a single-file, ready-to-run publish so downstream jobs only need the EXE.
|
||||||
try {
|
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
|
& $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 {
|
} catch {
|
||||||
Write-Host "dotnet publish failed: $_"
|
Write-Host "dotnet publish failed: $_"
|
||||||
throw
|
throw
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using System.Drawing;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace CatalogLite;
|
namespace CatalogLite;
|
||||||
|
|
||||||
|
|
@ -89,7 +90,7 @@ public sealed class CatalogConfigurationLoader
|
||||||
settings.Margine = values.GetInt("TestoMargine", 8);
|
settings.Margine = values.GetInt("TestoMargine", 8);
|
||||||
settings.LogoAltezza = values.GetInt("MarchioAltezza", 430);
|
settings.LogoAltezza = values.GetInt("MarchioAltezza", 430);
|
||||||
settings.LogoLarghezza = values.GetInt("MarchioLarghezza", 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.LogoAggiungi = values.GetBool("MarchioAggiungi");
|
||||||
settings.LogoNomeFile = values.GetString("MarchioFile");
|
settings.LogoNomeFile = values.GetString("MarchioFile");
|
||||||
settings.LogoTrasparenza = values.GetInt("MarchioTrasparenza", 100).ToString(CultureInfo.InvariantCulture);
|
settings.LogoTrasparenza = values.GetInt("MarchioTrasparenza", 100).ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
@ -118,7 +119,6 @@ public sealed class CatalogConfigurationLoader
|
||||||
settings.FotoRuotaASinistra = false;
|
settings.FotoRuotaASinistra = false;
|
||||||
settings.TempMinText = string.Empty;
|
settings.TempMinText = string.Empty;
|
||||||
settings.OverwriteFiles = values.GetBool("GeneraleSovrascriviFile");
|
settings.OverwriteFiles = values.GetBool("GeneraleSovrascriviFile");
|
||||||
settings.ImageCreatorProvider = "ImageSharp";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ImageCreationService.Options BuildOptions(ConfigurationValues values, string sourcePath, string destinationPath)
|
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);
|
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))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
|
|
@ -162,14 +162,19 @@ public sealed class CatalogConfigurationLoader
|
||||||
{
|
{
|
||||||
if (normalized.StartsWith('#') && normalized.Length == 7)
|
if (normalized.StartsWith('#') && normalized.Length == 7)
|
||||||
{
|
{
|
||||||
return Color.FromArgb(
|
return new Rgba32(
|
||||||
Convert.ToInt32(normalized[1..3], 16),
|
Convert.ToByte(normalized[1..3], 16),
|
||||||
Convert.ToInt32(normalized[3..5], 16),
|
Convert.ToByte(normalized[3..5], 16),
|
||||||
Convert.ToInt32(normalized[5..7], 16));
|
Convert.ToByte(normalized[5..7], 16),
|
||||||
|
255);
|
||||||
}
|
}
|
||||||
|
|
||||||
var named = Color.FromName(normalized);
|
if (normalized.Length == 6 && normalized.All(Uri.IsHexDigit))
|
||||||
return named.IsKnownColor || named.IsNamedColor ? named : fallback;
|
{
|
||||||
|
normalized = "#" + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color.Parse(normalized).ToPixel<Rgba32>();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
<RootNamespace>CatalogLite</RootNamespace>
|
<RootNamespace>CatalogLite</RootNamespace>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<CatalogLiteExpirationDate Condition="'$(CatalogLiteExpirationDate)' == ''">2026-12-31</CatalogLiteExpirationDate>
|
<CatalogLiteExpirationDate Condition="'$(CatalogLiteExpirationDate)' == ''">2026-12-31</CatalogLiteExpirationDate>
|
||||||
<EnableMaddoSharedGdi>false</EnableMaddoSharedGdi>
|
|
||||||
<UseAppHost>true</UseAppHost>
|
<UseAppHost>true</UseAppHost>
|
||||||
<SelfContained>false</SelfContained>
|
<SelfContained>false</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|
@ -27,7 +26,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" SetTargetFramework="TargetFramework=net10.0;EnableMaddoSharedGdi=false" />
|
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,6 @@ public sealed class LiteCatalogViewModel : ViewModelBase
|
||||||
_picSettings.DirectorySorgente = SourcePath;
|
_picSettings.DirectorySorgente = SourcePath;
|
||||||
_picSettings.DirectoryDestinazione = DestinationPath;
|
_picSettings.DirectoryDestinazione = DestinationPath;
|
||||||
_picSettings.DestDir = new DirectoryInfo(DestinationPath);
|
_picSettings.DestDir = new DirectoryInfo(DestinationPath);
|
||||||
_picSettings.ImageCreatorProvider = "ImageSharp";
|
|
||||||
|
|
||||||
IsProcessing = true;
|
IsProcessing = true;
|
||||||
ResetProgress("Analisi immagini...");
|
ResetProgress("Analisi immagini...");
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public class ChunkSizeBenchmarks
|
||||||
});
|
});
|
||||||
|
|
||||||
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
||||||
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorGDI>();
|
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorImageSharp>();
|
||||||
|
|
||||||
_picSettings = new PicSettings
|
_picSettings = new PicSettings
|
||||||
{
|
{
|
||||||
|
|
@ -75,7 +75,7 @@ public class ChunkSizeBenchmarks
|
||||||
Trasparenza = 100
|
Trasparenza = 100
|
||||||
};
|
};
|
||||||
|
|
||||||
var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger);
|
var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger);
|
||||||
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,32 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Imaging;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace MaddoShared.Benchmarks.Helpers;
|
namespace MaddoShared.Benchmarks.Helpers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper class to generate test images for benchmarking
|
/// Helper class to generate test images for benchmarking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
|
||||||
public static class TestImageGenerator
|
public static class TestImageGenerator
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Generates a set of test JPEG images in the specified directory
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputDirectory">Directory where images will be created</param>
|
|
||||||
/// <param name="imageCount">Number of images to generate</param>
|
|
||||||
/// <param name="width">Width of each image</param>
|
|
||||||
/// <param name="height">Height of each image</param>
|
|
||||||
/// <param name="includeSubfolders">Whether to create images in subfolders</param>
|
|
||||||
public static void GenerateTestImages(
|
public static void GenerateTestImages(
|
||||||
string outputDirectory,
|
string outputDirectory,
|
||||||
int imageCount,
|
int imageCount,
|
||||||
int width = 4000,
|
int width = 4000,
|
||||||
int height = 3000,
|
int height = 3000,
|
||||||
bool includeSubfolders = false)
|
bool includeSubfolders = false)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(outputDirectory);
|
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;
|
var targetDir = outputDirectory;
|
||||||
|
|
||||||
if (includeSubfolders && i % 10 == 0)
|
if (includeSubfolders && i % 10 == 0)
|
||||||
{
|
{
|
||||||
targetDir = Path.Combine(outputDirectory, $"Subfolder_{i / 10}");
|
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");
|
var filePath = Path.Combine(targetDir, $"test_image_{i:D5}.jpg");
|
||||||
|
|
||||||
// Skip if already exists
|
|
||||||
if (File.Exists(filePath))
|
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));
|
continue;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some text
|
using var image = new Image<Rgba32>(width, height, RandomColor(random));
|
||||||
using var font = new Font("Arial", 48, FontStyle.Bold);
|
AddBenchmarkTexture(image, random);
|
||||||
var text = $"Test Image {i}";
|
image.Save(filePath, encoder);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cleans up generated test images
|
|
||||||
/// </summary>
|
|
||||||
public static void CleanupTestImages(string directory)
|
public static void CleanupTestImages(string directory)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(directory))
|
if (Directory.Exists(directory))
|
||||||
|
|
@ -92,16 +53,38 @@ public static class TestImageGenerator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ImageCodecInfo GetEncoder(ImageFormat format)
|
private static void AddBenchmarkTexture(Image<Rgba32> image, Random random)
|
||||||
{
|
{
|
||||||
var codecs = ImageCodecInfo.GetImageEncoders();
|
image.ProcessPixelRows(accessor =>
|
||||||
foreach (var codec in codecs)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class ImageProcessingBenchmarks
|
||||||
private ImageCreationService _imageCreationStuff;
|
private ImageCreationService _imageCreationStuff;
|
||||||
private PicSettings _picSettings;
|
private PicSettings _picSettings;
|
||||||
private ILogger<ImageCreationService> _logger;
|
private ILogger<ImageCreationService> _logger;
|
||||||
private ILogger<ImageCreatorGDI> _imageCreatorLogger;
|
private ILogger<ImageCreatorImageSharp> _imageCreatorLogger;
|
||||||
|
|
||||||
[Params(10, 50, 100)]
|
[Params(10, 50, 100)]
|
||||||
public int ImageCount { get; set; }
|
public int ImageCount { get; set; }
|
||||||
|
|
@ -55,7 +55,7 @@ public class ImageProcessingBenchmarks
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger = loggerFactory.CreateLogger<ImageCreationService>();
|
_logger = loggerFactory.CreateLogger<ImageCreationService>();
|
||||||
_imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorGDI>();
|
_imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorImageSharp>();
|
||||||
|
|
||||||
// Setup PicSettings with default values
|
// Setup PicSettings with default values
|
||||||
_picSettings = new PicSettings
|
_picSettings = new PicSettings
|
||||||
|
|
@ -81,7 +81,7 @@ public class ImageProcessingBenchmarks
|
||||||
Trasparenza = 100
|
Trasparenza = 100
|
||||||
};
|
};
|
||||||
|
|
||||||
var imageCreatorService = new ImageCreatorGDI(_picSettings, _imageCreatorLogger);
|
var imageCreatorService = new ImageCreatorImageSharp(_picSettings, _imageCreatorLogger);
|
||||||
_imageCreationStuff = new ImageCreationService(_logger, _picSettings, imageCreatorService);
|
_imageCreationStuff = new ImageCreationService(_logger, _picSettings, imageCreatorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public class ImageSizeBenchmarks
|
||||||
});
|
});
|
||||||
|
|
||||||
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
||||||
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorGDI>();
|
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorImageSharp>();
|
||||||
|
|
||||||
_picSettings = new PicSettings
|
_picSettings = new PicSettings
|
||||||
{
|
{
|
||||||
|
|
@ -84,7 +84,7 @@ public class ImageSizeBenchmarks
|
||||||
Trasparenza = 100
|
Trasparenza = 100
|
||||||
};
|
};
|
||||||
|
|
||||||
var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger);
|
var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger);
|
||||||
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class StressTestBenchmark
|
||||||
|
|
||||||
Console.WriteLine($"[STRESS TEST] Generating {ImageCount} test images...");
|
Console.WriteLine($"[STRESS TEST] Generating {ImageCount} test images...");
|
||||||
Console.WriteLine("This may take several minutes depending on your hardware.");
|
Console.WriteLine("This may take several minutes depending on your hardware.");
|
||||||
|
|
||||||
// Use smaller images for stress test to save space and time
|
// Use smaller images for stress test to save space and time
|
||||||
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 1920, height: 1080);
|
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 1920, height: 1080);
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ public class StressTestBenchmark
|
||||||
});
|
});
|
||||||
|
|
||||||
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
var logger = loggerFactory.CreateLogger<ImageCreationService>();
|
||||||
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorGDI>();
|
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorImageSharp>();
|
||||||
|
|
||||||
_picSettings = new PicSettings
|
_picSettings = new PicSettings
|
||||||
{
|
{
|
||||||
|
|
@ -75,7 +75,7 @@ public class StressTestBenchmark
|
||||||
Trasparenza = 100
|
Trasparenza = 100
|
||||||
};
|
};
|
||||||
|
|
||||||
var imageCreatorService = new ImageCreatorGDI(_picSettings, imageCreatorLogger);
|
var imageCreatorService = new ImageCreatorImageSharp(_picSettings, imageCreatorLogger);
|
||||||
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
_imageCreationStuff = new ImageCreationService(logger, _picSettings, imageCreatorService);
|
||||||
|
|
||||||
Console.WriteLine($"[STRESS TEST] Setup complete. Ready to process {ImageCount} images.");
|
Console.WriteLine($"[STRESS TEST] Setup complete. Ready to process {ImageCount} images.");
|
||||||
|
|
@ -130,12 +130,12 @@ public class StressTestBenchmark
|
||||||
|
|
||||||
var results = new ConcurrentBag<string>();
|
var results = new ConcurrentBag<string>();
|
||||||
var startTime = DateTime.Now;
|
var startTime = DateTime.Now;
|
||||||
|
|
||||||
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
|
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
|
||||||
|
|
||||||
var duration = DateTime.Now - startTime;
|
var duration = DateTime.Now - startTime;
|
||||||
var throughput = ImageCount / duration.TotalSeconds;
|
var throughput = ImageCount / duration.TotalSeconds;
|
||||||
|
|
||||||
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
|
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
|
||||||
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
|
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
|
||||||
}
|
}
|
||||||
|
|
@ -160,12 +160,12 @@ public class StressTestBenchmark
|
||||||
|
|
||||||
var results = new ConcurrentBag<string>();
|
var results = new ConcurrentBag<string>();
|
||||||
var startTime = DateTime.Now;
|
var startTime = DateTime.Now;
|
||||||
|
|
||||||
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
|
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
|
||||||
|
|
||||||
var duration = DateTime.Now - startTime;
|
var duration = DateTime.Now - startTime;
|
||||||
var throughput = ImageCount / duration.TotalSeconds;
|
var throughput = ImageCount / duration.TotalSeconds;
|
||||||
|
|
||||||
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
|
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
|
||||||
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
|
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="3.0.0" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
<PackageReference Include="SixLabors.Fonts" Version="3.0.0" />
|
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,237 +1,63 @@
|
||||||
using System;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Imaging;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using Shouldly;
|
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Shouldly;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
|
||||||
namespace MaddoShared.Tests
|
namespace MaddoShared.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class ImageCreatorSharpTests
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestMethod]
|
||||||
public class ImageCreatorSharpTests
|
public void CalculateThumbnailSize_Larghezza_UsesWidthScaling()
|
||||||
{
|
{
|
||||||
private ImageCreatorGDI CreateService(Action<PicSettings> 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
|
text,
|
||||||
{
|
"Arial",
|
||||||
DimStandard = 20,
|
40,
|
||||||
DimStandardMiniatura = 10,
|
50f,
|
||||||
LarghezzaSmall = 100,
|
20f,
|
||||||
AltezzaSmall = 100,
|
6
|
||||||
LarghezzaBig = 800,
|
})!;
|
||||||
AltezzaBig = 600,
|
|
||||||
Trasparenza = 50,
|
|
||||||
IlFont = "Arial",
|
|
||||||
Grassetto = false,
|
|
||||||
Posizione = "CENTRO",
|
|
||||||
Allineamento = "CENTRO",
|
|
||||||
Margine = 10,
|
|
||||||
MargVert = 10,
|
|
||||||
TestoMin = false,
|
|
||||||
AggNumTempMin = false
|
|
||||||
};
|
|
||||||
|
|
||||||
customize?.Invoke(settings);
|
size.ShouldBeInRange(6f, 40f);
|
||||||
|
(size * text.Length * 0.6f <= 50f || size <= 6f).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
var logger = Substitute.For<ILogger<ImageCreatorGDI>>();
|
private static Size CalculateThumbnailSize(int width, int height, int maxPixel, string sizeMode)
|
||||||
return new ImageCreatorGDI(settings, logger);
|
{
|
||||||
}
|
var method = typeof(ImageCreatorImageSharp).GetMethod(
|
||||||
|
"CalculateThumbnailSize",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
|
||||||
[TestMethod]
|
method.ShouldNotBeNull();
|
||||||
public void CalculateThumbnailSize_Larghezza_UsesWidthScaling()
|
return (Size)method.Invoke(null, new object[] { width, height, maxPixel, sizeMode })!;
|
||||||
{
|
|
||||||
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<object>());
|
|
||||||
res.ShouldBeFalse();
|
|
||||||
|
|
||||||
svc = CreateService(s => s.TestoMin = true);
|
|
||||||
mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
||||||
res = (bool)mi.Invoke(svc, Array.Empty<object>());
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -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<ImageCreatorGDI> 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<ushort> 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<string> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ''' Aggiunge Orario, tempo gara e altri
|
|
||||||
/// ''' </summary>
|
|
||||||
/// ''' <param name="g">Image</param>
|
|
||||||
/// <param name="imgState"></param>
|
|
||||||
/// ''' <remarks></remarks>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ''' Prepara diverse variabili azzerandole, elaborandole e prendendole dalle impostazioni
|
|
||||||
/// ''' </summary>
|
|
||||||
/// ''' <remarks></remarks>
|
|
||||||
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 */;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ''' Calculate the Size of the New image
|
|
||||||
/// ''' </summary>
|
|
||||||
/// ''' <param name="currentwidth">Larghezza</param>
|
|
||||||
/// ''' <param name="currentheight">Altezza</param>
|
|
||||||
/// ''' <param name="maxPixel"></param>
|
|
||||||
/// ''' <param name="tipoSize"></param>
|
|
||||||
/// ''' <returns></returns>
|
|
||||||
/// ''' <remarks></remarks>
|
|
||||||
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
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
// System.Drawing not required for ImageSharp-based drawing in this class
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
|
@ -17,11 +16,8 @@ using SixLabors.ImageSharp.Drawing;
|
||||||
namespace MaddoShared;
|
namespace MaddoShared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Image creator implemented using SixLabors.ImageSharp for core image operations.
|
/// Image creator implemented with SixLabors.ImageSharp for loading, EXIF orientation,
|
||||||
/// This implementation focuses on loading, EXIF-orientation, resizing and saving.
|
/// resizing, text/logo drawing, and saving.
|
||||||
/// It intentionally implements a minimal subset of the original functionality to
|
|
||||||
/// provide a safe and testable replacement. Additional features (text/logo drawing)
|
|
||||||
/// can be added later using ImageSharp.Drawing.Common and SixLabors.Fonts.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ImageCreatorImageSharp : IImageCreator
|
public class ImageCreatorImageSharp : IImageCreator
|
||||||
{
|
{
|
||||||
|
|
@ -38,12 +34,11 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(imgState);
|
ArgumentNullException.ThrowIfNull(imgState);
|
||||||
|
|
||||||
// Minimal preparation of names and settings normally done by ImageCreatorSharp.PrepareVariables
|
|
||||||
PrepareVariablesMinimal(imgState);
|
PrepareVariablesMinimal(imgState);
|
||||||
|
|
||||||
try
|
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);
|
using var fs = File.OpenRead(imgState.WorkFile.FullName);
|
||||||
|
|
||||||
|
|
@ -59,9 +54,6 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
// text to draw (horizontal vs vertical).
|
// text to draw (horizontal vs vertical).
|
||||||
ApplyExifOrientation(img, imgState);
|
ApplyExifOrientation(img, imgState);
|
||||||
|
|
||||||
// Determine output format
|
|
||||||
var forceJpg = _picSettings.UsaForzaJpg;
|
|
||||||
|
|
||||||
// Compute big size
|
// Compute big size
|
||||||
var bigSize = ComputeBigSize(img.Width, img.Height);
|
var bigSize = ComputeBigSize(img.Width, img.Height);
|
||||||
|
|
||||||
|
|
@ -71,10 +63,10 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
// Ensure destination exists
|
// Ensure destination exists
|
||||||
imgState.DestDir?.Create();
|
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
|
// 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
|
// Create thumbnail if requested
|
||||||
if (_picSettings.CreaMiniature)
|
if (_picSettings.CreaMiniature)
|
||||||
|
|
@ -85,20 +77,16 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall);
|
var fileNameSmall = System.IO.Path.Combine(imgState.DestDir.FullName, imgState.NomeFileSmall);
|
||||||
|
|
||||||
// Draw overlays and save thumbnail via ImageSharp
|
// 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)
|
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;
|
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)
|
private static SixLabors.ImageSharp.Formats.IImageEncoder GetEncoderForExtension(string ext, long quality)
|
||||||
{
|
{
|
||||||
quality = Math.Clamp(quality, 1, 100);
|
quality = Math.Clamp(quality, 1, 100);
|
||||||
|
|
@ -110,7 +98,7 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DrawAndSaveWithGdiAsync(Image<Rgba32> imgSharp, string outputPath, ImageState imgState, byte[]? logoData, long quality, bool isThumbnail)
|
private async Task DrawAndSaveAsync(Image<Rgba32> 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.
|
// 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.
|
// Clone editable image so we don't mutate the original reference unexpectedly.
|
||||||
|
|
@ -125,13 +113,13 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
// Ensure DataFoto is set (extracted earlier) so time-based text is available
|
||||||
imgState.DataFoto = imgState.CreationDate ?? DateTime.Now;
|
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 (isThumbnail)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola))
|
if (string.IsNullOrEmpty(imgState.TestoFirmaPiccola))
|
||||||
|
|
@ -285,8 +273,7 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw logo if provided. For compatibility with the original GDI implementation,
|
// Draw logos only on full-size images.
|
||||||
// do not draw the logo on thumbnails (ImageCreatorSharp only draws logos on big images).
|
|
||||||
if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail)
|
if (logoData != null && logoData.Length > 0 && _picSettings.LogoAggiungi && !isThumbnail)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -414,7 +401,7 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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)
|
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);
|
await working.SaveAsync(outStream, encoder).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed GDI encoder helper; ImageSharp encoders are used instead.
|
|
||||||
|
|
||||||
private void PrepareVariablesMinimal(ImageState imgState)
|
private void PrepareVariablesMinimal(ImageState imgState)
|
||||||
{
|
{
|
||||||
imgState.NomeFileBig = imgState.WorkFile.Name;
|
imgState.NomeFileBig = imgState.WorkFile.Name;
|
||||||
|
|
@ -454,7 +439,6 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
imgState.DimensioneStandardMiniatura = _picSettings.DimStandardMiniatura;
|
imgState.DimensioneStandardMiniatura = _picSettings.DimStandardMiniatura;
|
||||||
|
|
||||||
// basic text / transparency defaults used by drawing routines
|
// 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));
|
imgState.AlphaScelta = Convert.ToInt32((255 * (100 - _picSettings.Trasparenza) / (double)100));
|
||||||
|
|
||||||
// Set minimal text fields so text drawing has fallback values
|
// Set minimal text fields so text drawing has fallback values
|
||||||
|
|
@ -517,9 +501,7 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
|
|
||||||
private void ApplyExifOrientation(Image<Rgba32> img, ImageState imgState)
|
private void ApplyExifOrientation(Image<Rgba32> img, ImageState imgState)
|
||||||
{
|
{
|
||||||
// Common EXIF orientations: 1=TopLeft, 3=BottomRight (rotate 180), 6=RightTop (rotate 90 CW), 8=LeftBottom (rotate 270 CW)
|
// Set rotation flags on the state so other code can pick the correct text variant.
|
||||||
// Set rotation flags on the state so other code can pick the correct
|
|
||||||
// text variant (vertical vs horizontal). Mirror ImageCreatorSharp logic.
|
|
||||||
imgState.FotoRuotaADestra = false;
|
imgState.FotoRuotaADestra = false;
|
||||||
imgState.FotoRuotaASinistra = false;
|
imgState.FotoRuotaASinistra = false;
|
||||||
|
|
||||||
|
|
@ -567,11 +549,11 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Non-fatal: log and continue
|
// 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
|
// If original large size option requested, return original
|
||||||
// otherwise compute based on width/height limits from settings
|
// otherwise compute based on width/height limits from settings
|
||||||
|
|
@ -580,16 +562,14 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
: CalculateThumbnailSize(width, height, _picSettings.AltezzaBig, "Altezza");
|
: 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
|
return width > height
|
||||||
? CalculateThumbnailSize(width, height, _picSettings.LarghezzaSmall, "Larghezza")
|
? CalculateThumbnailSize(width, height, _picSettings.LarghezzaSmall, "Larghezza")
|
||||||
: CalculateThumbnailSize(width, height, _picSettings.AltezzaSmall, "Altezza");
|
: CalculateThumbnailSize(width, height, _picSettings.AltezzaSmall, "Altezza");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to access PicSettings values via instance _picSettings
|
private static Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize)
|
||||||
|
|
||||||
private static System.Drawing.Size CalculateThumbnailSize(int currentwidth, int currentheight, int maxPixel, string tipoSize)
|
|
||||||
{
|
{
|
||||||
double tempMultiplier;
|
double tempMultiplier;
|
||||||
if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(tipoSize, "Larghezza", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -601,7 +581,7 @@ public class ImageCreatorImageSharp : IImageCreator
|
||||||
else
|
else
|
||||||
tempMultiplier = maxPixel / (double)currentwidth;
|
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;
|
return newSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MaddoShared;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dynamically resolves the concrete IImageCreator implementation at call time
|
|
||||||
/// based on current PicSettings.ImageCreatorProvider.
|
|
||||||
/// On non-Windows platforms only ImageCreatorImageSharp is available.
|
|
||||||
/// </summary>
|
|
||||||
public class ImageCreatorMapper : IImageCreator
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _sp;
|
|
||||||
private readonly PicSettings _settings;
|
|
||||||
private readonly ILogger<ImageCreatorMapper> _logger;
|
|
||||||
|
|
||||||
public ImageCreatorMapper(IServiceProvider sp, PicSettings settings, ILogger<ImageCreatorMapper> 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<ImageCreatorImageSharp>(imgState, logoData)
|
|
||||||
: ResolveAndCall<ImageCreatorGDI>(imgState, logoData);
|
|
||||||
#else
|
|
||||||
// GDI is not available on non-Windows — always use ImageSharp
|
|
||||||
return ResolveAndCall<ImageCreatorImageSharp>(imgState, logoData);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task ResolveAndCall<T>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace MaddoShared;
|
namespace MaddoShared;
|
||||||
|
|
@ -28,18 +27,16 @@ public class ImageState
|
||||||
public DateTime DataPartenzaI { get; set; }
|
public DateTime DataPartenzaI { get; set; }
|
||||||
public string TestoOrario { get; set; }
|
public string TestoOrario { get; set; }
|
||||||
public string TestoFirmaPiccola { get; set; }
|
public string TestoFirmaPiccola { get; set; }
|
||||||
public Size ThumbSizeSmall { get; set; }
|
public string NomeFileSmall { get; set; }
|
||||||
public Size ThumbSizeBig { get; set; }
|
public string NomeFileBig { get; set; }
|
||||||
public string NomeFileSmall{ get; set; }
|
public string NomeFileBig2 { get; set; }
|
||||||
public string NomeFileBig{ get; set; }
|
|
||||||
public string NomeFileBig2{ get; set; }
|
|
||||||
|
|
||||||
public float YPosFromBottom{ get; set; }
|
public float YPosFromBottom { get; set; }
|
||||||
public float YPosFromBottom1{ get; set; }
|
public float YPosFromBottom1 { get; set; }
|
||||||
public float YPosFromBottom2{ get; set; }
|
public float YPosFromBottom2 { get; set; }
|
||||||
public float YPosFromBottom3{ get; set; }
|
public float YPosFromBottom3 { get; set; }
|
||||||
public float YPosFromBottom4{ get; set; }
|
public float YPosFromBottom4 { get; set; }
|
||||||
|
|
||||||
public Orientations Orientation{ get; set; }
|
public Orientations Orientation { get; set; }
|
||||||
public DateTime? CreationDate{ get; set; }
|
public DateTime? CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net10.0;net10.0-windows</TargetFrameworks>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<EnableMaddoSharedGdi Condition="'$(EnableMaddoSharedGdi)' == '' and '$(TargetFramework)' == 'net10.0-windows'">true</EnableMaddoSharedGdi>
|
|
||||||
<EnableMaddoSharedGdi Condition="'$(EnableMaddoSharedGdi)' == ''">false</EnableMaddoSharedGdi>
|
|
||||||
<!-- WINDOWS preprocessor symbol enables the optional GDI image creator. -->
|
|
||||||
<DefineConstants Condition="'$(EnableMaddoSharedGdi)' == 'true'">$(DefineConstants);WINDOWS</DefineConstants>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncEnumerator" Version="4.0.2" />
|
<PackageReference Include="AsyncEnumerator" Version="4.0.2" />
|
||||||
|
|
@ -33,6 +29,5 @@
|
||||||
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
|
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.3" Condition="'$(EnableMaddoSharedGdi)' == 'true'" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace MaddoShared;
|
namespace MaddoShared;
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ public class PicSettings
|
||||||
public int Margine { get; set; }
|
public int Margine { get; set; }
|
||||||
public int LogoAltezza { get; set; }
|
public int LogoAltezza { get; set; }
|
||||||
public int LogoLarghezza { 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; }
|
public bool LogoAggiungi { get; set; }
|
||||||
// Initialize logo-related strings to safe defaults to avoid null reference issues
|
// Initialize logo-related strings to safe defaults to avoid null reference issues
|
||||||
public string LogoNomeFile { get; set; } = string.Empty;
|
public string LogoNomeFile { get; set; } = string.Empty;
|
||||||
|
|
@ -81,6 +81,4 @@ public class PicSettings
|
||||||
public bool FotoRuotaASinistra { get; set; } = false;
|
public bool FotoRuotaASinistra { get; set; } = false;
|
||||||
public string TempMinText { get; set; } = string.Empty;
|
public string TempMinText { get; set; } = string.Empty;
|
||||||
public bool OverwriteFiles { get; set; } = false;
|
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";
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ Catalog 3
|
||||||
The build embeds an expiration date from the `CatalogLiteExpirationDate` MSBuild property:
|
The build embeds an expiration date from the `CatalogLiteExpirationDate` MSBuild property:
|
||||||
|
|
||||||
```powershell
|
```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.
|
The separate Forgejo workflow is `.forgejo/workflows/build-catalog-lite.yml`; run it manually and set `expiration_date` in `yyyy-MM-dd` format.
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ Verification approach
|
||||||
Scope boundaries
|
Scope boundaries
|
||||||
----------------
|
----------------
|
||||||
- In scope: `ImageCreatorImageSharp` behavior: resize, EXIF rotation, text presence/position, logo position/opactiy, thumbnails.
|
- 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
|
Implementation notes
|
||||||
--------------------
|
--------------------
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,6 @@
|
||||||
<TextBlock Text="Chunk:" VerticalAlignment="Center" Grid.Column="2" />
|
<TextBlock Text="Chunk:" VerticalAlignment="Center" Grid.Column="2" />
|
||||||
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="74" Grid.Column="3" />
|
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="74" Grid.Column="3" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock Text="Libreria Immagini" FontWeight="Bold" />
|
|
||||||
<StackPanel Margin="0,2,0,0" Spacing="3">
|
|
||||||
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" IsVisible="{Binding IsRunningOnWindows}" />
|
|
||||||
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Spacing="8">
|
<StackPanel Grid.Column="1" Spacing="8">
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
#if WINDOWS
|
|
||||||
using System.Drawing.Text;
|
|
||||||
#endif
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -23,6 +19,7 @@ using AutoMapper;
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
|
||||||
namespace ImageCatalog_2
|
namespace ImageCatalog_2
|
||||||
{
|
{
|
||||||
|
|
@ -561,16 +558,19 @@ namespace ImageCatalog_2
|
||||||
|
|
||||||
private List<string> LoadAvailableFonts()
|
private List<string> LoadAvailableFonts()
|
||||||
{
|
{
|
||||||
#if WINDOWS
|
try
|
||||||
var fonts = new List<string>();
|
|
||||||
using (var installedFonts = new InstalledFontCollection())
|
|
||||||
{
|
{
|
||||||
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<string>();
|
||||||
}
|
}
|
||||||
return fonts;
|
|
||||||
#else
|
|
||||||
return new List<string>();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource? _mainToken;
|
private CancellationTokenSource? _mainToken;
|
||||||
|
|
@ -841,56 +841,6 @@ namespace ImageCatalog_2
|
||||||
set => _visual.LogoTransparency = value;
|
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";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the application is running on Windows. Used by cross-platform UIs to show/hide Windows-only options.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsRunningOnWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The selected image processing library. Possible values: "System.Graphics" or "ImageSharp".
|
|
||||||
/// This value is mirrored into PicSettings.ImageCreatorProvider so the runtime mapper picks the implementation.
|
|
||||||
/// </summary>
|
|
||||||
public string ImageLibrary
|
|
||||||
{
|
|
||||||
get => _imageLibrary;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_imageLibrary == value) return;
|
|
||||||
_imageLibrary = value;
|
|
||||||
// Reflect selection into PicSettings so mapper can resolve at runtime
|
|
||||||
_picSettings.ImageCreatorProvider = string.Equals(value, "ImageSharp", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? "ALTERNATE"
|
|
||||||
: "Sharp";
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
NotifyPropertyChanged(nameof(UseSystemGraphics));
|
|
||||||
NotifyPropertyChanged(nameof(UseImageSharp));
|
|
||||||
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
|
// Folder division settings
|
||||||
private int _filesPerFolder = 99;
|
private int _filesPerFolder = 99;
|
||||||
public int FilesPerFolder
|
public int FilesPerFolder
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -72,6 +72,7 @@
|
||||||
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
|
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
<PackageReference Include="IconPacks.Avalonia" Version="2.0.0" />
|
<PackageReference Include="IconPacks.Avalonia" Version="2.0.0" />
|
||||||
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Drawing;
|
using AutoMapper;
|
||||||
using AutoMapper;
|
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace ImageCatalog_2.Mappings;
|
namespace ImageCatalog_2.Mappings;
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ public class DataModelMappingProfile : Profile
|
||||||
// Paths
|
// Paths
|
||||||
.ForMember(dest => dest.DirectorySorgente, opt => opt.MapFrom(src => src.SourcePath))
|
.ForMember(dest => dest.DirectorySorgente, opt => opt.MapFrom(src => src.SourcePath))
|
||||||
.ForMember(dest => dest.DirectoryDestinazione, opt => opt.MapFrom(src => src.DestinationPath))
|
.ForMember(dest => dest.DirectoryDestinazione, opt => opt.MapFrom(src => src.DestinationPath))
|
||||||
|
|
||||||
// Font and text settings
|
// Font and text settings
|
||||||
.ForMember(dest => dest.DimStandard, opt => opt.MapFrom(src => src.FontSize))
|
.ForMember(dest => dest.DimStandard, opt => opt.MapFrom(src => src.FontSize))
|
||||||
.ForMember(dest => dest.DimStandardMiniatura, opt => opt.MapFrom(src => src.FontSizeThumbnail))
|
.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.Allineamento, opt => opt.MapFrom(src => src.HorizontalAlignment))
|
||||||
.ForMember(dest => dest.Trasparenza, opt => opt.MapFrom(src => src.TextTransparency))
|
.ForMember(dest => dest.Trasparenza, opt => opt.MapFrom(src => src.TextTransparency))
|
||||||
.ForMember(dest => dest.Margine, opt => opt.MapFrom(src => src.TextMargin))
|
.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
|
// Thumbnail settings
|
||||||
.ForMember(dest => dest.AltezzaSmall, opt => opt.MapFrom(src => src.ThumbnailHeight))
|
.ForMember(dest => dest.AltezzaSmall, opt => opt.MapFrom(src => src.ThumbnailHeight))
|
||||||
.ForMember(dest => dest.LarghezzaSmall, opt => opt.MapFrom(src => src.ThumbnailWidth))
|
.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.CreaMiniature, opt => opt.MapFrom(src => src.CreateThumbnails))
|
||||||
.ForMember(dest => dest.JpegQualityMin, opt => opt.MapFrom(src => src.JpegQualityThumbnail))
|
.ForMember(dest => dest.JpegQualityMin, opt => opt.MapFrom(src => src.JpegQualityThumbnail))
|
||||||
.ForMember(dest => dest.DimMin, opt => opt.MapFrom(src => src.FontSizeThumbnail))
|
.ForMember(dest => dest.DimMin, opt => opt.MapFrom(src => src.FontSizeThumbnail))
|
||||||
|
|
||||||
// Big photo settings
|
// Big photo settings
|
||||||
.ForMember(dest => dest.AltezzaBig, opt => opt.MapFrom(src => src.PhotoBigHeight))
|
.ForMember(dest => dest.AltezzaBig, opt => opt.MapFrom(src => src.PhotoBigHeight))
|
||||||
.ForMember(dest => dest.LarghezzaBig, opt => opt.MapFrom(src => src.PhotoBigWidth))
|
.ForMember(dest => dest.LarghezzaBig, opt => opt.MapFrom(src => src.PhotoBigWidth))
|
||||||
.ForMember(dest => dest.FotoGrandeDimOrigina, opt => opt.MapFrom(src => src.KeepOriginalDimensions))
|
.ForMember(dest => dest.FotoGrandeDimOrigina, opt => opt.MapFrom(src => src.KeepOriginalDimensions))
|
||||||
.ForMember(dest => dest.JpegQuality, opt => opt.MapFrom(src => src.JpegQuality))
|
.ForMember(dest => dest.JpegQuality, opt => opt.MapFrom(src => src.JpegQuality))
|
||||||
.ForMember(dest => dest.Codice, opt => opt.MapFrom(src => src.BigPhotoSuffix))
|
.ForMember(dest => dest.Codice, opt => opt.MapFrom(src => src.BigPhotoSuffix))
|
||||||
|
|
||||||
// Logo settings
|
// Logo settings
|
||||||
.ForMember(dest => dest.LogoAggiungi, opt => opt.MapFrom(src => src.AddLogo))
|
.ForMember(dest => dest.LogoAggiungi, opt => opt.MapFrom(src => src.AddLogo))
|
||||||
.ForMember(dest => dest.LogoNomeFile, opt => opt.MapFrom(src => src.LogoFile))
|
.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.LogoTrasparenza, opt => opt.MapFrom(src => src.LogoTransparency.ToString()))
|
||||||
.ForMember(dest => dest.LogoPosizioneH, opt => opt.MapFrom(src => src.LogoHorizontalPosition))
|
.ForMember(dest => dest.LogoPosizioneH, opt => opt.MapFrom(src => src.LogoHorizontalPosition))
|
||||||
.ForMember(dest => dest.LogoPosizioneV, opt => opt.MapFrom(src => src.LogoVerticalPosition))
|
.ForMember(dest => dest.LogoPosizioneV, opt => opt.MapFrom(src => src.LogoVerticalPosition))
|
||||||
|
|
||||||
// Text content
|
// Text content
|
||||||
.ForMember(dest => dest.TestoFirmaStart, opt => opt.MapFrom(src => src.HorizontalText))
|
.ForMember(dest => dest.TestoFirmaStart, opt => opt.MapFrom(src => src.HorizontalText))
|
||||||
.ForMember(dest => dest.TestoFirmaStartV, opt => opt.MapFrom(src => src.VerticalText))
|
.ForMember(dest => dest.TestoFirmaStartV, opt => opt.MapFrom(src => src.VerticalText))
|
||||||
|
|
||||||
// Vertical text settings
|
// Vertical text settings
|
||||||
.ForMember(dest => dest.DimVert, opt => opt.MapFrom(src => src.VerticalTextSize))
|
.ForMember(dest => dest.DimVert, opt => opt.MapFrom(src => src.VerticalTextSize))
|
||||||
.ForMember(dest => dest.MargVert, opt => opt.MapFrom(src => src.VerticalTextMargin))
|
.ForMember(dest => dest.MargVert, opt => opt.MapFrom(src => src.VerticalTextMargin))
|
||||||
|
|
||||||
// Boolean flags
|
// Boolean flags
|
||||||
.ForMember(dest => dest.UsaRotazioneAutomatica, opt => opt.MapFrom(src => src.AutomaticRotation))
|
.ForMember(dest => dest.UsaRotazioneAutomatica, opt => opt.MapFrom(src => src.AutomaticRotation))
|
||||||
.ForMember(dest => dest.UsaForzaJpg, opt => opt.MapFrom(src => src.ForceJpeg))
|
.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.UsaOrarioTestoApplicare, opt => opt.MapFrom(src => src.AddTime))
|
||||||
.ForMember(dest => dest.UsaTempoGaraTestoApplicare, opt => opt.MapFrom(src => src.AddRaceTime))
|
.ForMember(dest => dest.UsaTempoGaraTestoApplicare, opt => opt.MapFrom(src => src.AddRaceTime))
|
||||||
.ForMember(dest => dest.OverwriteFiles, opt => opt.MapFrom(src => src.OverwriteImages))
|
.ForMember(dest => dest.OverwriteFiles, opt => opt.MapFrom(src => src.OverwriteImages))
|
||||||
|
|
||||||
// Additional settings
|
// Additional settings
|
||||||
.ForMember(dest => dest.UsaOrarioMiniatura, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.Time))
|
.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.DataPartenza, opt => opt.MapFrom(src => src.RaceStartDate))
|
||||||
.ForMember(dest => dest.TestoOrario, opt => opt.MapFrom(src => src.TimeLabel))
|
.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))
|
.ForMember(dest => dest.TestoMin, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.FileName))
|
||||||
|
|
||||||
// Thumbnail text options
|
// Thumbnail text options
|
||||||
.ForMember(dest => dest.AggiungiScritteMiniature, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.Text))
|
.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.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))
|
.ForMember(dest => dest.AggNumTempMin, opt => opt.MapFrom(src => src.ThumbnailOption == ImageCatalog_2.DataModel.ThumbnailOptionEnum.FileNameAndTime))
|
||||||
|
|
||||||
// Ignore unmapped properties
|
// Ignore unmapped properties
|
||||||
.ForMember(dest => dest.DestDir, opt => opt.Ignore())
|
.ForMember(dest => dest.DestDir, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.SecretDefault, 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.FotoRuotaASinistra, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.TempMinText, 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<Rgba32>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new Rgba32(255, 255, 0, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,11 +192,6 @@ namespace ImageCatalog_2.Models
|
||||||
[XmlElement("UsaColoreTrasparente")]
|
[XmlElement("UsaColoreTrasparente")]
|
||||||
public bool UseTransparentColor { get; set; } = false;
|
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
|
// Options
|
||||||
[JsonPropertyName("ForceJpeg")]
|
[JsonPropertyName("ForceJpeg")]
|
||||||
[XmlElement("GeneraleForzaJpg")]
|
[XmlElement("GeneraleForzaJpg")]
|
||||||
|
|
|
||||||
|
|
@ -143,13 +143,7 @@ static class Program
|
||||||
services.AddTransient<IAiExtractionService, AiExtractionService>();
|
services.AddTransient<IAiExtractionService, AiExtractionService>();
|
||||||
services.AddTransient<IImageProcessingCoordinator, ImageProcessingCoordinator>();
|
services.AddTransient<IImageProcessingCoordinator, ImageProcessingCoordinator>();
|
||||||
services.AddTransient<ImageCreationService>();
|
services.AddTransient<ImageCreationService>();
|
||||||
#if WINDOWS
|
services.AddTransient<IImageCreator, ImageCreatorImageSharp>();
|
||||||
services.AddTransient<ImageCreatorGDI>();
|
|
||||||
#endif
|
|
||||||
services.AddTransient<ImageCreatorImageSharp>();
|
|
||||||
services.AddTransient<ImageCreatorMapper>();
|
|
||||||
|
|
||||||
services.AddTransient<IImageCreator>(sp => sp.GetRequiredService<ImageCreatorMapper>());
|
|
||||||
|
|
||||||
var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
"ImageCatalog", "userprefs.xml");
|
"ImageCatalog", "userprefs.xml");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue