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