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