Compare commits

...

10 commits

Author SHA1 Message Date
0d6435520b Merge branch 'avalonia' into 'develop'
Enhance image processing performance and flexibility by introducing atomic...

See merge request MaddoScientisto/Catalog!2
2026-03-14 08:48:17 +00:00
b194a2424c feat: Update Microsoft.Extensions packages to version 10.0.5 for improved functionality and stability 2026-03-14 09:44:21 +01:00
901dc02ca8 feat: Enhance UI with new styles, improved layout, and icon integration across various views 2026-03-12 23:40:41 +01:00
fa09f7c324 feat: Update FaceAiTabView to support output file selection and validation for .pkl files 2026-03-12 23:24:44 +01:00
41d9dacfac feat: Replace Moq and FluentAssertions with NSubstitute and Shouldly in test projects 2026-03-12 19:40:58 +01:00
3c722a66df feat: Add AI extraction service and related view models
- Introduced `IAiExtractionService` and its implementation `AiExtractionService` for processing images and extracting text.
- Created `AiResultItem` model to hold results from AI extraction.
- Added `ImageProcessingCoordinator` to manage image processing tasks and provide progress updates.
- Implemented view models for AI settings, path settings, processing state, race upload settings, and visual settings to support UI binding.
- Updated `Program.cs` to register new services and dependencies.
- Modified project file to skip MinVer execution during local builds.
2026-03-12 18:48:13 +01:00
bdf503c627 feat: Refactor RaceUploadCommunicationClient and RaceUploadTabView to improve HttpClient management and resource disposal 2026-03-08 14:30:37 +01:00
b29cc95a1e feat: Enhance Avalonia UI with compact styles and improved layout for various views; adjust margins and paddings for a denser interface 2026-03-08 13:44:09 +01:00
e80b427fcc feat: Update Avalonia UI with new tab icons and styles; upgrade AutoMapper and Avalonia packages 2026-03-08 12:09:58 +01:00
d62342aae1 Implement ImageCreatorImageSharp using SixLabors.ImageSharp for image processing
- Added ImageCreatorImageSharp class for image creation, handling EXIF orientation, resizing, and saving images.
- Replaced GDI+ dependencies with ImageSharp for cross-platform compatibility.
- Introduced methods for drawing text and logos on images, including handling transparency and positioning.
- Created a test plan for validating ImageCreatorImageSharp functionality, focusing on image resizing, text positioning, logo features, and EXIF orientation.
- Added documentation for the test plan outlining goals, project structure, and implementation notes.
2026-03-08 11:17:47 +01:00
45 changed files with 2603 additions and 1417 deletions

1
.gitignore vendored
View file

@ -255,3 +255,4 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
.vscode/settings.json

View file

@ -2,6 +2,8 @@ using System.Net;
using Catalog.Communication.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace Catalog.Communication.DependencyInjection;
@ -25,22 +27,30 @@ public static class CatalogCommunicationServiceCollectionExtensions
services.TryAddSingleton<CookieContainer>();
services
.AddHttpClient<IRaceUploadCommunicationClient, RaceUploadCommunicationClient>((sp, client) =>
// Create the HttpClient only when the communication client is requested.
// This avoids constructing the DefaultHttpClientFactory (and its background cleanup timer)
// if the race-upload feature is never used.
services.AddTransient<IRaceUploadCommunicationClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>().Value;
var logger = sp.GetService<ILogger<RaceUploadCommunicationClient>>() ?? NullLogger<RaceUploadCommunicationClient>.Instance;
var cookieContainer = sp.GetRequiredService<CookieContainer>();
var handler = new HttpClientHandler
{
var options = sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>().Value;
client.BaseAddress = options.BaseUri;
})
.ConfigurePrimaryHttpMessageHandler(sp =>
UseCookies = true,
CookieContainer = cookieContainer,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
};
var httpClient = new HttpClient(handler, disposeHandler: true)
{
var cookieContainer = sp.GetRequiredService<CookieContainer>();
return new HttpClientHandler
{
UseCookies = true,
CookieContainer = cookieContainer,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
};
});
BaseAddress = options.BaseUri,
Timeout = options.RequestTimeout,
};
return new RaceUploadCommunicationClient(httpClient, sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>(), logger);
});
return services;
}

View file

@ -10,7 +10,7 @@ using Microsoft.Extensions.Options;
namespace Catalog.Communication;
public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient
public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient, IDisposable
{
private const string AdminMenuPath = "admin/menu/Menu4.abl";
private const string PublicLogonPath = "Logon.abl";
@ -27,6 +27,7 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie
private readonly HttpClient _httpClient;
private readonly ILogger<RaceUploadCommunicationClient> _logger;
private readonly IOptions<CatalogCommunicationOptions> _options;
private bool _disposed;
public RaceUploadCommunicationClient(
HttpClient httpClient,
@ -38,6 +39,18 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie
_logger = logger;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_httpClient.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
public Task<RawEndpointResponse> LoginAdminAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);

View file

@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "M
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.ImageSharpTests", "MaddoShared.ImageSharpTests\MaddoShared.ImageSharpTests.csproj", "{1528903F-3BF9-599C-2DD0-0AF7B5706675}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -87,14 +89,26 @@ Global
{EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x64.Build.0 = Release|Any CPU
{EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.ActiveCfg = Release|Any CPU
{EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.Build.0 = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.ActiveCfg = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.Build.0 = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.ActiveCfg = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.Build.0 = Debug|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.Build.0 = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.ActiveCfg = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.Build.0 = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.ActiveCfg = Release|Any CPU
{1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AEBFE9E3-277C-4A7B-8448-145D1B11998B} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9}
{EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9}
{59952BE8-20B4-4BF2-9367-705F41395265} = {5F0BEF23-B1EA-4100-A772-DC455D40B1C1}
{EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757}

View file

@ -0,0 +1,44 @@
using System.IO;
using Microsoft.Extensions.Logging.Abstractions;
using SixLabors.ImageSharp.PixelFormats;
namespace MaddoShared.ImageSharpTests.Helpers
{
public static class CreatorFactory
{
public static MaddoShared.PicSettings CreateDefaultPicSettings()
{
return new MaddoShared.PicSettings
{
DimStandard = 48,
DimStandardMiniatura = 12,
LarghezzaSmall = 150,
AltezzaSmall = 150,
LarghezzaBig = 800,
AltezzaBig = 600,
Trasparenza = 0,
IlFont = "Arial",
Grassetto = false,
Posizione = "CENTRO",
Allineamento = "CENTRO",
Margine = 10,
MargVert = 10,
TestoMin = false,
AggNumTempMin = false,
CreaMiniature = false,
LogoAggiungi = false,
LogoAltezza = 100,
LogoLarghezza = 100,
LogoMargine = "0",
JpegQuality = 90,
JpegQualityMin = 75,
};
}
public static MaddoShared.ImageCreatorImageSharp CreateImageCreator(MaddoShared.PicSettings settings)
{
var logger = NullLogger<MaddoShared.ImageCreatorImageSharp>.Instance;
return new MaddoShared.ImageCreatorImageSharp(settings, logger);
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace MaddoShared.ImageSharpTests.Helpers
{
public static class PixelInspector
{
public static int CountNonBackgroundPixels(string path, int x, int y, int width, int height, Rgba32 background, int tolerance = 0)
{
using var img = SixLabors.ImageSharp.Image.Load<Rgba32>(path);
var bx = Math.Max(0, x);
var by = Math.Max(0, y);
var bw = Math.Min(width, img.Width - bx);
var bh = Math.Min(height, img.Height - by);
if (bw <= 0 || bh <= 0) return 0;
int count = 0;
img.ProcessPixelRows(accessor =>
{
for (int yy = by; yy < by + bh; yy++)
{
var row = accessor.GetRowSpan(yy);
for (int xx = bx; xx < bx + bw; xx++)
{
var p = row[xx];
if (!IsApproximatelyEqual(p, background, tolerance)) count++;
}
}
});
return count;
}
private static bool IsApproximatelyEqual(Rgba32 a, Rgba32 b, int tol)
{
return Math.Abs(a.R - b.R) <= tol && Math.Abs(a.G - b.G) <= tol && Math.Abs(a.B - b.B) <= tol && Math.Abs(a.A - b.A) <= tol;
}
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.IO;
namespace MaddoShared.ImageSharpTests.Helpers
{
public sealed class TempWorkspace : IDisposable
{
public DirectoryInfo Root { get; }
public DirectoryInfo SourceDir { get; }
public DirectoryInfo DestDir { get; }
public TempWorkspace()
{
var root = Path.Combine(Path.GetTempPath(), "MaddoShared.ImageSharpTests", Guid.NewGuid().ToString("N"));
Root = Directory.CreateDirectory(root);
SourceDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Source"));
DestDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Dest"));
}
public void Dispose()
{
try
{
if (Root.Exists)
Root.Delete(true);
}
catch
{
// best-effort cleanup
}
}
}
}

View file

@ -0,0 +1,45 @@
using System.IO;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace MaddoShared.ImageSharpTests.Helpers
{
public static class TestImageFactory
{
public static string CreateSolidJpeg(string directory, string fileName, int width, int height, Rgba32 color)
{
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, fileName);
using var img = new Image<Rgba32>(width, height, color);
var encoder = new JpegEncoder { Quality = 90 };
img.Save(path, encoder);
return path;
}
public static string CreateSolidPng(string directory, string fileName, int width, int height, Rgba32 color)
{
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, fileName);
using var img = new Image<Rgba32>(width, height, color);
img.SaveAsPng(path);
return path;
}
public static string CreateJpegWithExifOrientation(string directory, string fileName, int width, int height, Rgba32 color, ushort orientation)
{
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, fileName);
using var img = new Image<Rgba32>(width, height, color);
// Add EXIF orientation
var profile = new ExifProfile();
profile.SetValue(ExifTag.Orientation, orientation);
img.Metadata.ExifProfile = profile;
var encoder = new JpegEncoder { Quality = 90 };
img.Save(path, encoder);
return path;
}
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,44 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SixLabors.ImageSharp.PixelFormats;
using MaddoShared.ImageSharpTests.Helpers;
using Shouldly;
namespace MaddoShared.ImageSharpTests.Tests
{
[TestClass]
public class ImageResizingTests
{
[TestMethod]
public async Task BigImageResizesRespectSettings()
{
using var ws = new TempWorkspace();
// create a large input image
var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 1600, 1200, new Rgba32(200, 200, 200, 255));
var pic = CreatorFactory.CreateDefaultPicSettings();
pic.LarghezzaBig = 800;
pic.AltezzaBig = 600;
pic.CreaMiniature = false;
var svc = CreatorFactory.CreateImageCreator(pic);
var state = new MaddoShared.ImageState
{
WorkFile = new FileInfo(inputPath),
DestDir = ws.DestDir,
SourceDir = ws.SourceDir
};
await svc.CreateImageAsync(state, null);
var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig);
using var outImg = SixLabors.ImageSharp.Image.Load<Rgba32>(outPath);
outImg.Width.ShouldBe(800);
outImg.Height.ShouldBe(600);
}
}
}

View file

@ -0,0 +1,51 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SixLabors.ImageSharp.PixelFormats;
using MaddoShared.ImageSharpTests.Helpers;
using Shouldly;
namespace MaddoShared.ImageSharpTests.Tests
{
[TestClass]
public class TextPositioningTests
{
[TestMethod]
public async Task TextAtBottom_IncreasesNonBackgroundPixelCountInBottomBand()
{
using var ws = new TempWorkspace();
// create white background input
var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 800, 600, new Rgba32(255, 255, 255, 255));
var pic = CreatorFactory.CreateDefaultPicSettings();
pic.Posizione = "BASSO";
pic.DimStandard = 48; // big text
pic.TestoFirmaStart = "SAMPLE TEXT";
pic.CreaMiniature = false;
var svc = CreatorFactory.CreateImageCreator(pic);
var state = new MaddoShared.ImageState
{
WorkFile = new FileInfo(inputPath),
DestDir = ws.DestDir,
SourceDir = ws.SourceDir
};
await svc.CreateImageAsync(state, null);
var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig);
// bottom band (lower 25% of image)
var bottomY = (int)(600 * 0.75);
var bottomCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, bottomY, 800, 600 - bottomY, new Rgba32(255, 255, 255, 255), tolerance: 10);
// top band (upper 25%)
var topCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, 0, 800, (int)(600 * 0.25), new Rgba32(255, 255, 255, 255), tolerance: 10);
(bottomCount > 50).ShouldBeTrue($"Expected text pixels in bottom band, found {bottomCount}");
(bottomCount > topCount).ShouldBeTrue("Expected more non-background pixels at bottom than top");
}
}
}

View file

@ -0,0 +1,172 @@
using System;
using System.Threading.Tasks;
using ImageCatalog_2;
using ImageCatalog_2.Services;
using MaddoShared;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using Shouldly;
namespace MaddoShared.Tests;
[TestClass]
public class DataModelCharacterizationTests
{
[TestMethod]
public void SelectSourceFolderCommand_RaisesEvent()
{
var model = CreateModel();
var raised = false;
model.SelectSourceFolderRequested += (_, _) => raised = true;
model.SelectSourceFolderCommand.Execute(null);
raised.ShouldBeTrue();
}
[TestMethod]
public async Task SaveSettingsToFileAsync_DelegatesToSettingsService()
{
var settingsService = Substitute.For<ISettingsService>();
settingsService
.SaveSettingsAsync(Arg.Any<string>(), Arg.Any<object>())
.Returns(Task.CompletedTask);
var model = CreateModel(settingsService: settingsService);
await model.SaveSettingsToFileAsync("settings.xml");
await settingsService.Received(1)
.SaveSettingsAsync("settings.xml", model);
}
[TestMethod]
public async Task LoadSettingsFromFileAsync_DelegatesToSettingsService()
{
var settingsService = Substitute.For<ISettingsService>();
settingsService
.LoadSettingsAsync(Arg.Any<string>(), Arg.Any<object>())
.Returns(Task.CompletedTask);
var model = CreateModel(settingsService: settingsService);
await model.LoadSettingsFromFileAsync("settings.xml");
await settingsService.Received(1)
.LoadSettingsAsync("settings.xml", model);
}
[TestMethod]
public void ThumbnailOptionIndex_UpdatesAuthoritativeThumbnailState()
{
var model = CreateModel();
model.ThumbnailOptionIndex = (int)DataModel.ThumbnailOptionEnum.RaceTime;
model.ThumbnailOption.ShouldBe(DataModel.ThumbnailOptionEnum.RaceTime);
model.AddRaceTimeToThumbnails.ShouldBeTrue();
model.ThumbnailMode.ShouldBe("RaceTime");
}
[TestMethod]
public void ProcessingChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Processing.SpeedCounter = "12.00 f/s";
changed.ShouldBe(nameof(DataModel.SpeedCounter));
model.SpeedCounter.ShouldBe("12.00 f/s");
}
[TestMethod]
public void PathsNormalize_UpdatesFlattenedSourceAndDestination()
{
var model = CreateModel();
model.SourcePath = "\"C:/input\"";
model.DestinationPath = "C:/output";
model.Paths.NormalizePaths();
model.SourcePath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}input{System.IO.Path.DirectorySeparatorChar}");
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
}
[TestMethod]
public void AiChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Ai.ModelsFolderPath = "K:/models";
changed.ShouldBe(nameof(DataModel.ModelsFolderPath));
model.ModelsFolderPath.ShouldBe("K:/models");
}
[TestMethod]
public void RaceUploadChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.RaceUpload.ApiLogin = "admin";
changed.ShouldBe(nameof(DataModel.ApiLogin));
model.ApiLogin.ShouldBe("admin");
}
[TestMethod]
public void VisualChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Visual.FontSize = 42;
changed.ShouldBe(nameof(DataModel.FontSize));
model.FontSize.ShouldBe(42);
}
private static DataModel CreateModel(
ISettingsService? settingsService = null,
ITestService? testService = null)
{
var mapper = Substitute.For<AutoMapper.IMapper>();
var picSettings = new PicSettings();
var imageCreator = Substitute.For<IImageCreator>();
imageCreator
.CreateImageAsync(Arg.Any<ImageState>(), Arg.Any<byte[]?>())
.Returns(Task.CompletedTask);
var imageCreationService = new ImageCreationService(
Substitute.For<ILogger<ImageCreationService>>(),
picSettings,
imageCreator);
var imageProcessingCoordinator = new ImageProcessingCoordinator(
imageCreationService,
Substitute.For<ILogger<ImageProcessingCoordinator>>());
var aiExtractionService = new AiExtractionService(
Substitute.For<ILogger<AiExtractionService>>());
return new DataModel(
testService ?? Substitute.For<ITestService>(),
settingsService ?? Substitute.For<ISettingsService>(),
imageCreationService,
aiExtractionService,
imageProcessingCoordinator,
picSettings,
mapper,
Substitute.For<ILogger<DataModel>>(),
versionProvider: null);
}
}

View file

@ -5,8 +5,8 @@ using System.IO;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.Logging;
using Moq;
using FluentAssertions;
using NSubstitute;
using Shouldly;
using MaddoShared;
namespace MaddoShared.Tests
@ -37,7 +37,7 @@ namespace MaddoShared.Tests
customize?.Invoke(settings);
var logger = new Mock<ILogger<ImageCreatorGDI>>().Object;
var logger = Substitute.For<ILogger<ImageCreatorGDI>>();
return new ImageCreatorGDI(settings, logger);
}
@ -46,12 +46,12 @@ namespace MaddoShared.Tests
{
var svc = CreateService();
var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var size = (Size)mi.Invoke(svc, new object[] { 400, 200, 200, "Larghezza" });
size.Width.Should().Be(200);
size.Height.Should().Be(100);
size.Width.ShouldBe(200);
size.Height.ShouldBe(100);
}
[TestMethod]
@ -59,12 +59,12 @@ namespace MaddoShared.Tests
{
var svc = CreateService();
var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var size = (Size)mi.Invoke(svc, new object[] { 200, 400, 200, "Altezza" });
size.Width.Should().Be(100);
size.Height.Should().Be(200);
size.Width.ShouldBe(100);
size.Height.ShouldBe(200);
}
[TestMethod]
@ -72,13 +72,13 @@ namespace MaddoShared.Tests
{
var svc = CreateService();
var mi = svc.GetType().GetMethod("IsSameDirectory", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
bool same = (bool)mi.Invoke(svc, new object[] { @"C:\Temp", @"c:\temp" });
same.Should().BeTrue();
same.ShouldBeTrue();
bool notSame = (bool)mi.Invoke(svc, new object[] { @"C:\TempA", @"c:\temp" });
notSame.Should().BeFalse();
notSame.ShouldBeFalse();
}
[TestMethod]
@ -86,12 +86,12 @@ namespace MaddoShared.Tests
{
var svc = CreateService(s => s.Codice = "_X");
var mi = svc.GetType().GetMethod("UpdateFilenameWithCode", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var state = new ImageState { NomeFileSmall = "photo123.jpg" };
mi.Invoke(svc, new object[] { state });
state.NomeFileSmall.Should().Be("photo123_X.jpg");
state.NomeFileSmall.ShouldBe("photo123_X.jpg");
}
[DataTestMethod]
@ -103,16 +103,16 @@ namespace MaddoShared.Tests
var svc = CreateService(s => { s.Allineamento = alignment; s.Margine = 20; });
var mi = svc.GetType().GetMethod("CalculateHorizontalAlignment", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var center = (float)mi.Invoke(svc, new object[] { 800, 100f });
if (alignment == "SINISTRA")
center.Should().BeInRange(0f, 400f, "Expected left alignment range");
center.ShouldBeInRange(0f, 400f);
if (alignment == "DESTRA")
center.Should().BeInRange(400f, 800f, "Expected right alignment range");
center.ShouldBeInRange(400f, 800f);
if (alignment == "CENTRO")
center.Should().BeApproximately(800 / 2f, 0.0001f);
center.ShouldBe(800 / 2f, 0.0001f);
}
[TestMethod]
@ -120,14 +120,14 @@ namespace MaddoShared.Tests
{
var svc = CreateService(s => s.Posizione = "ALTO");
var mi = svc.GetType().GetMethod("SetVerticalPosition", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var state = new ImageState();
// ALTO
mi.Invoke(svc, new object[] { 500, 20f, state });
state.YPosFromBottom1.Should().Be(10f);
state.YPosFromBottom4.Should().Be(10f);
state.YPosFromBottom1.ShouldBe(10f);
state.YPosFromBottom4.ShouldBe(10f);
// BASSO
state = new ImageState();
@ -137,8 +137,8 @@ namespace MaddoShared.Tests
var expected1 = (float)(200 - 20 - (200 * 10 / 100.0));
var expected4 = (float)(200 - 20 - (200 * 5 / 100.0));
state.YPosFromBottom1.Should().BeApproximately(expected1, 0.001f);
state.YPosFromBottom4.Should().BeApproximately(expected4, 0.001f);
state.YPosFromBottom1.ShouldBe(expected1, 0.001f);
state.YPosFromBottom4.ShouldBe(expected4, 0.001f);
}
[TestMethod]
@ -146,7 +146,7 @@ namespace MaddoShared.Tests
{
var svc = CreateService();
var mi = svc.GetType().GetMethod("FormatTimeText", BindingFlags.NonPublic | BindingFlags.Instance);
mi.Should().NotBeNull();
mi.ShouldNotBeNull();
var state = new ImageState
{
@ -156,13 +156,13 @@ namespace MaddoShared.Tests
DataFoto = new DateTime(2024, 01, 01, 11, 59, 0)
};
var withoutName = (string)mi.Invoke(svc, new object[] { state, false });
withoutName.Should().StartWith(Environment.NewLine);
withoutName.Should().Contain("T:");
withoutName.ShouldStartWith(Environment.NewLine);
withoutName.ShouldContain("T:");
var withName = (string)mi.Invoke(svc, new object[] { state, true });
withName.Should().Contain("file.jpg");
withName.Should().Contain("T:");
withName.Should().Contain(Environment.NewLine);
withName.ShouldContain("file.jpg");
withName.ShouldContain("T:");
withName.ShouldContain(Environment.NewLine);
}
[TestMethod]
@ -170,18 +170,18 @@ namespace MaddoShared.Tests
{
var svc = CreateService();
var miPrep = svc.GetType().GetMethod("PrepareSignatureText", BindingFlags.NonPublic | BindingFlags.Instance);
miPrep.Should().NotBeNull();
miPrep.ShouldNotBeNull();
var state = new ImageState { NomeFileBig = "bigname.jpg" };
svc = CreateService(s => s.TestoMin = true);
miPrep.Invoke(svc, new object[] { state });
state.TestoFirmaPiccola.Should().Be("bigname.jpg");
state.TestoFirmaPiccola.ShouldBe("bigname.jpg");
state.TestoFirmaPiccola = "";
svc = CreateService(s => { s.TestoMin = false; s.AggNumTempMin = true; });
miPrep.Invoke(svc, new object[] { state });
state.TestoFirmaPiccola.Should().Be("bigname.jpg ");
state.TestoFirmaPiccola.ShouldBe("bigname.jpg ");
}
[TestMethod]
@ -189,15 +189,15 @@ namespace MaddoShared.Tests
{
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.Should().NotBeNull();
mi.ShouldNotBeNull();
var res = (bool)mi.Invoke(svc, Array.Empty<object>());
res.Should().BeFalse();
res.ShouldBeFalse();
svc = CreateService(s => s.TestoMin = true);
mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance);
res = (bool)mi.Invoke(svc, Array.Empty<object>());
res.Should().BeTrue();
res.ShouldBeTrue();
}
[TestMethod]
@ -209,24 +209,29 @@ namespace MaddoShared.Tests
using var g = Graphics.FromImage(bmp);
var miFind = svc.GetType().GetMethod("FindBestFontSize", BindingFlags.NonPublic | BindingFlags.Instance);
miFind.Should().NotBeNull();
miFind.ShouldNotBeNull();
int best = (int)miFind.Invoke(svc, new object[] { g, "A very long text that won't fit", "Arial", 40, false, 50, 5 });
best.Should().BeInRange(5, 40);
var miAdjust = svc.GetType().GetMethod("AdjustFontToFitWidth", BindingFlags.NonPublic | BindingFlags.Instance);
miAdjust.Should().NotBeNull();
best.ShouldBeInRange(5, 40);
// The helper AdjustFontToFitWidth was in an earlier refactor; replicate its logic here
var imageState = new ImageState { DimensioneStandardMiniatura = 30, TestoFirmaPiccola = "A very long test string" };
var initialFont = new Font("Arial", imageState.DimensioneStandardMiniatura);
var textSize = g.MeasureString(imageState.TestoFirmaPiccola, initialFont);
object[] parameters = new object[] { g, 50, imageState, textSize };
miAdjust.Invoke(svc, parameters);
int tempFontSize = imageState.DimensioneStandardMiniatura;
while ((textSize.Width > 50) && tempFontSize > 5)
{
tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1;
using var tempFont = new Font("Arial", tempFontSize);
textSize = g.MeasureString(imageState.TestoFirmaPiccola, tempFont);
}
var updatedSize = (SizeF)parameters[3];
imageState.DimensioneStandardMiniatura.Should().BeLessThanOrEqualTo(30);
(updatedSize.Width <= 50 || imageState.DimensioneStandardMiniatura <= 5).Should().BeTrue();
var updatedSize = textSize;
imageState.DimensioneStandardMiniatura = tempFontSize;
imageState.DimensioneStandardMiniatura.ShouldBeLessThanOrEqualTo(30);
(updatedSize.Width <= 50 || imageState.DimensioneStandardMiniatura <= 5).ShouldBeTrue();
}
}
}

View file

@ -15,13 +15,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\imagecatalog\ImageCatalog 2.csproj" />
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
</ItemGroup>

View file

@ -228,23 +228,23 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
}
}
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;
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);
PrepareSignatureText(imgState);
if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione))
UpdateFilenameWithCode(imgState);
if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione))
UpdateFilenameWithCode(imgState);
if (ShouldRenderText())
CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format);
else
CreateSimpleThumbnail(sourceImage, imgState, format);
}
if (ShouldRenderText())
CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format);
else
CreateSimpleThumbnail(sourceImage, imgState, format);
}
private void PrepareSignatureText(ImageState imgState)
{
@ -294,7 +294,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
// 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;
@ -375,20 +375,20 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
switch (picSettings.Posizione.ToUpper())
{
case "ALTO":
{
imgState.YPosFromBottom = picSettings.Margine;
imgState.YPosFromBottom3 = picSettings.MargVert;
break;
}
{
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;
}
{
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;
@ -396,27 +396,27 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
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;
}
{
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;
}
{
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;
}
{
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;
@ -528,7 +528,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
{
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 },
@ -571,44 +571,44 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
{
case "SINISTRA":
case "NESSUNA":
{
xPosOfWm = margineUsato;
break;
}
{
xPosOfWm = margineUsato;
break;
}
case "CENTRO":
{
xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2);
break;
}
{
xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2);
break;
}
case "DESTRA":
{
xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato);
break;
}
{
xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato);
break;
}
}
switch (logoV)
{
case "ALTO":
case "NESSUNA":
{
yPosOfWm = margineUsato;
break;
}
{
yPosOfWm = margineUsato;
break;
}
case "CENTRO":
{
yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2);
break;
}
{
yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2);
break;
}
case "BASSO":
{
yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato);
break;
}
{
yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato);
break;
}
}
grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0,
@ -776,7 +776,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
{
// 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":
@ -787,18 +787,18 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
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;

View file

@ -0,0 +1,85 @@
# Image generation test plan — ImageSharp-only (multiplatform)
Goal
-----
Create an automated, cross-platform test project that validates `ImageCreatorImageSharp` behavior by generating synthetic input images and pixel-inspecting outputs produced by the library.
Decisions
---------
- Test only `ImageCreatorImageSharp` (multiplatform).
- Test project targets `net10.0` (not Windows-only) and uses SixLabors.ImageSharp for both generation and verification; avoid `System.Drawing.Common` in tests.
- Inputs are programmatically generated images (no checked-in large binaries).
- Verification uses pixel inspection (sample regions, color averages, non-background pixel counts).
Project
-------
- Name: `MaddoShared.ImageSharpTests` (folder: `MaddoShared.ImageSharpTests/`)
- TargetFramework: `net10.0` (no Windows-only flags)
- Package references:
- `MSTest.TestFramework` / `MSTest.TestAdapter` / `Microsoft.NET.Test.Sdk`
- `FluentAssertions`
- `Moq` (if needed for loggers)
- `SixLabors.ImageSharp` and `SixLabors.ImageSharp.Drawing` (for image creation and pixel inspection)
- `Microsoft.Extensions.Logging.Abstractions` (lightweight logging)
- ProjectReference: `../MaddoShared/MaddoShared.csproj`
Helpers (tests)
----------------
- `TempWorkspace` — creates temporary `Source` and `Dest` folders, cleans up on dispose.
- `TestImageFactory` — creates synthetic JPEG/PNG inputs and in-memory logo PNG bytes. Also can write EXIF orientation values.
- `PixelInspector` — loads output using ImageSharp and exposes:
- `CountNonBackgroundPixels(path, Rectangle region, Rgba32 background, int tolerance)`
- `SampleAverageColor(path, Rectangle region)`
- Region helpers: top/bottom/left/right/center/quadrant rectangles for given image sizes
- `CreatorFactory` — builds `PicSettings` defaults and `ImageCreatorImageSharp` instances.
Test cases (high-level)
-----------------------
1. Image resizing: verify big and small output dimensions respect settings.
2. Text positioning: for `Posizione` = ALTO/CENTRO/BASSO and `Allineamento` = SINISTRA/CENTRO/DESTRA assert text pixels appear in expected regions.
3. Text content: baseline (empty) vs non-empty comparisons to ensure text changes output; EXIF-vertical photo selects `TestoFirmaV`.
4. Logo positioning: use a solid-color logo (e.g., pure red PNG) and verify red pixels appear in the expected quadrant for combinations of `LogoPosizioneH` × `LogoPosizioneV` and with margins (absolute and percentage).
5. Logo features: opacity (logo color blending) and color-key transparency.
6. EXIF orientation: ensure rotation is applied and output contains no EXIF orientation tag.
Verification approach
---------------------
- For text: compare non-background pixel count in the target band vs opposite band (thresholded) to avoid brittle exact glyph placement checks.
- For logo: sample the quadrant where the logo should be; count logo-colored pixels and assert above threshold.
- For opacity: sample average color in logo region and assert it is blended when opacity <100%.
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.
Implementation notes
--------------------
- Keep tests deterministic by creating solid-color inputs and simple logos.
- Use modest image sizes (e.g., 800×600) so tests run fast.
- Carefully choose thresholds for pixel-count assertions to be robust across fonts and rendering differences.
Run & validate
---------------
- Build: `dotnet build MaddoShared.ImageSharpTests`
- Test: `dotnet test MaddoShared.ImageSharpTests`
Files to add
------------
- `MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj`
- `MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs`
- `MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs`
- `MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs`
- `MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs`
- `MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs`
- `MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs`
- `MaddoShared.ImageSharpTests/Tests/LogoPositioningTests.cs`
- `MaddoShared.ImageSharpTests/Tests/ExifOrientationTests.cs`
- `docs/image-generation-tests-plan.md` (this file)
Next steps
----------
1. Create the `MaddoShared.ImageSharpTests` project and add the helper files.
2. Implement the first set of tests (resizing + a simple text presence test) to validate the testing harness.
3. Iterate thresholds and add remaining tests.

View file

@ -1,8 +1,106 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaApp">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="AppWindowBackgroundBrush">#F3F5F8</SolidColorBrush>
<SolidColorBrush x:Key="AppSurfaceBackgroundBrush">#FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="AppControlBackgroundBrush">#F7F8FA</SolidColorBrush>
<SolidColorBrush x:Key="AppControlBackgroundBrushPointerOver">#EEF2F6</SolidColorBrush>
<SolidColorBrush x:Key="AppBorderBrush">#D2D8E0</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="AppWindowBackgroundBrush">#1B2027</SolidColorBrush>
<SolidColorBrush x:Key="AppSurfaceBackgroundBrush">#242A33</SolidColorBrush>
<SolidColorBrush x:Key="AppControlBackgroundBrush">#2D343F</SolidColorBrush>
<SolidColorBrush x:Key="AppControlBackgroundBrushPointerOver">#38414E</SolidColorBrush>
<SolidColorBrush x:Key="AppBorderBrush">#4B5563</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<FluentTheme DensityStyle="Compact" />
<Style Selector="Window">
<Setter Property="Background" Value="{DynamicResource AppWindowBackgroundBrush}" />
</Style>
<Style Selector="Border">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
</Style>
<!-- Compact default styles to reduce padding/margins for a denser UI -->
<Style Selector="TabItem">
<Setter Property="Padding" Value="4,2" />
<Setter Property="Margin" Value="0" />
</Style>
<!-- Make tab header title text smaller and remove extra margin -->
<Style Selector="TabItem TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TabControl">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
</Style>
<Style Selector="TextBox">
<Setter Property="Padding" Value="8,4" />
<Setter Property="Margin" Value="0" />
<Setter Property="MinHeight" Value="30" />
<Setter Property="Background" Value="{DynamicResource AppControlBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="10,4" />
<Setter Property="Margin" Value="0" />
<Setter Property="MinHeight" Value="30" />
<Setter Property="Background" Value="{DynamicResource AppControlBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{DynamicResource AppControlBackgroundBrushPointerOver}" />
</Style>
<Style Selector="ComboBox">
<Setter Property="MinHeight" Value="30" />
<Setter Property="Background" Value="{DynamicResource AppControlBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
</Style>
<Style Selector="CheckBox">
<Setter Property="Margin" Value="0,1,0,0" />
<Setter Property="Padding" Value="2,0" />
</Style>
<Style Selector="RadioButton">
<Setter Property="Margin" Value="0,1,0,0" />
<Setter Property="Padding" Value="2,0" />
</Style>
<Style Selector="TextBlock">
<Setter Property="Margin" Value="0,4,0,0" />
</Style>
<Style Selector="StackPanel">
<Setter Property="Spacing" Value="4" />
</Style>
<Style Selector="ProgressBar">
<Setter Property="MinHeight" Value="16" />
</Style>
<StyleInclude Source="avares://IconPacks.Avalonia/Icons.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
</Application>

View file

@ -1,92 +1,165 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
x:Class="ImageCatalog_2.AvaloniaMainWindow"
mc:Ignorable="d"
Title="Image Catalog - Avalonia" Height="540" Width="800">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaMainWindow"
mc:Ignorable="d"
Title="Image Catalog - Avalonia" Height="540" Width="800">
<Grid RowDefinitions="*,Auto">
<Grid Grid.Row="0" Margin="10">
<Grid Grid.Row="0" Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="0.8*" />
</Grid.ColumnDefinitions>
<TabControl Grid.Column="0" Margin="0,0,10,0">
<TabItem Header="Generale">
<TabControl Grid.Column="0" Margin="0,0,6,0">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CogOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Generale" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:GeneralTabView />
</TabItem>
<TabItem Header="Testo">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="FormatLetterCase" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Testo" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:TextTabView />
</TabItem>
<TabItem Header="Foto">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CameraFrontVariant" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Foto" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:PhotoTabView />
</TabItem>
<TabItem Header="Miniature">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Image" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Miniature" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:ThumbnailsTabView />
</TabItem>
<TabItem Header="Logo">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="ImageFilterCenterFocus" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Logo" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:LogoTabView />
</TabItem>
<TabItem Header="AI">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Robot" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="AI" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:AiTabView />
</TabItem>
<TabItem Header="Face AI">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="FaceAgent" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Face AI" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:FaceAiTabView />
</TabItem>
<TabItem Header="Race Upload">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CloudUploadOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Race Upload" VerticalAlignment="Center" />
</StackPanel>
</TabItem.Header>
<views:RaceUploadTabView />
</TabItem>
</TabControl>
<StackPanel Grid.Column="1">
<StackPanel HorizontalAlignment="Right" Margin="0,0,0,12">
<Button Name="ThemeToggleButton" Width="28" Height="28" Click="ToggleTheme_Click" ToolTip.Tip="Cambia tema"
HorizontalAlignment="Right" Padding="2" Content="🌙" />
<Grid Grid.Column="1" RowDefinitions="Auto,*">
<StackPanel HorizontalAlignment="Right" Margin="0,0,0,8">
<Button Name="ThemeToggleButton" Width="28" Height="28" Click="ToggleTheme_Click" ToolTip.Tip="Cambia tema"
HorizontalAlignment="Right" Padding="2">
<iconPacks:PackIconMaterial Kind="ThemeLightDark" Width="14" Height="14" Foreground="{DynamicResource ForegroundBrush}" />
</Button>
</StackPanel>
<Border BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
<StackPanel>
<StackPanel>
<Button Width="120" Margin="0,0,0,8" Command="{Binding LoadSettingsCommand}" Content="Carica" />
<Button Width="120" Margin="0,0,0,8" Command="{Binding SaveSettingsCommand}" Content="Salva" />
<Button Width="120" Height="36" Margin="0,6,0,8"
<Border Grid.Row="1" BorderThickness="1" Padding="10" MaxWidth="280" MinWidth="0">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*" RowSpacing="8" MinWidth="0">
<StackPanel Grid.Row="0">
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Command="{Binding LoadSettingsCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<iconPacks:PackIconMaterial Kind="FolderUploadOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Carica" />
</StackPanel>
</Button>
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Command="{Binding SaveSettingsCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<iconPacks:PackIconMaterial Kind="ContentSaveOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
<TextBlock Text="Salva" />
</StackPanel>
</Button>
<Button HorizontalAlignment="Stretch" Height="34" Margin="0,4,0,4"
Command="{Binding ProcessImagesCommand}"
IsEnabled="{Binding UiEnabled}" Content="Avvia" />
<Button Width="120" Height="36"
Command="{Binding AsyncCancelOperationCommand}" Content="Stop" />
IsEnabled="{Binding UiEnabled}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<iconPacks:PackIconMaterial Kind="PlayCircleOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="Green" />
<TextBlock Text="Avvia" />
</StackPanel>
</Button>
<Button HorizontalAlignment="Stretch" Height="34"
Command="{Binding AsyncCancelOperationCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<iconPacks:PackIconMaterial Kind="StopCircleOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="Red" />
<TextBlock Text="Stop" />
</StackPanel>
</Button>
</StackPanel>
<Separator Margin="0,12,0,12" />
<Separator Grid.Row="1" Margin="0,4,0,0" />
<TextBlock Text="Stato" FontWeight="Bold" />
<TextBlock Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
<TextBlock Grid.Row="2" Text="Stato" FontWeight="Bold" />
<TextBlock Grid.Row="3" Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
<TextBlock Text="Progresso" FontWeight="Bold" Margin="0,8,0,0" />
<ProgressBar Minimum="0" Maximum="{Binding ProgressBarMaximum}"
Value="{Binding ProgressBarValue}" Height="20" />
<TextBlock Margin="0,6,0,0">
<TextBlock Grid.Row="4" Text="Progresso" FontWeight="Bold" Margin="0,4,0,0" />
<ProgressBar Grid.Row="5" Minimum="0" Maximum="{Binding ProgressBarMaximum}"
Value="{Binding ProgressBarValue}" Height="18" HorizontalAlignment="Stretch" MinWidth="0" />
<TextBlock Grid.Row="6" Margin="0,2,0,0">
<Run Text="{Binding ProcessedImagesCount}" />
<Run Text=" / " />
<Run Text="{Binding TotalImagesCount}" />
</TextBlock>
<TextBlock Text="Velocita" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
<TextBlock Grid.Row="7" Text="Velocita" FontWeight="Bold" Margin="0,4,0,0" />
<StackPanel Grid.Row="8" VerticalAlignment="Top">
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
<TextBlock Text="{Binding AppVersion}" Margin="0,8,0,0" Opacity="0.6" />
</StackPanel>
<TextBlock Text="{Binding AppVersion}" Margin="0,8,0,0" Opacity="0.6" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Grid>
</Grid>
</Grid>
</Window>

View file

@ -155,12 +155,6 @@ public partial class AvaloniaMainWindow : Window
private void UpdateThemeToggleButtonContent()
{
var toggleButton = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
if (toggleButton is null)
{
return;
}
toggleButton.Content = _isDarkTheme ? "☀" : "🌙";
_ = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
}
}

View file

@ -1,40 +1,75 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
<ScrollViewer>
<StackPanel Margin="8">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,8,0,0" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,12,0,0" />
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
Click="OpenModelsFolder_Click" Content="Apri" />
</Grid>
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="4">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,12,0,0" />
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
Click="OpenCsvOutputFolder_Click" Content="Apri" />
</Grid>
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Margin="6,0,0,0" Grid.Column="3"
Click="OpenModelsFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,12,0,0" />
<avaloniaDataGrid:DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
AutoGenerateColumns="False" Height="200" Margin="0,6,0,0">
<avaloniaDataGrid:DataGrid.Columns>
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
</avaloniaDataGrid:DataGrid.Columns>
</avaloniaDataGrid:DataGrid>
</StackPanel>
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
<Button Content="Avvia AI" Command="{Binding StartAiCommand}" Width="120" />
<Button Content="Annulla" Command="{Binding AsyncCancelOperationCommand}" Width="120" />
</StackPanel>
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Margin="6,0,0,0" Grid.Column="3"
Click="OpenCsvOutputFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,4,0,4" />
</StackPanel>
</ScrollViewer>
<avaloniaDataGrid:DataGrid Grid.Row="1" ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
AutoGenerateColumns="False" Margin="4,4,4,4" CanUserResizeColumns="True" VerticalAlignment="Stretch">
<avaloniaDataGrid:DataGrid.Columns>
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
</avaloniaDataGrid:DataGrid.Columns>
</avaloniaDataGrid:DataGrid>
</Grid>
</UserControl>

View file

@ -1,34 +1,66 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView">
<ScrollViewer>
<StackPanel Margin="8" Spacing="8">
<StackPanel Margin="4" Spacing="6">
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
<TextBlock Text="Esegue face_encoder.exe usando la cartella Destinazione corrente come --images."
<TextBlock Text="Esegue face_encoder.exe usando la cartella Destinazione corrente come --images e un file .pkl come --out."
TextWrapping="Wrap" Opacity="0.8" />
<TextBlock Text="Eseguibile" FontWeight="Bold" Margin="0,6,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
<TextBlock Text="Eseguibile" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder.exe" />
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Content="Scegli..." Click="SelectFaceExecutable_Click" Width="88" />
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Content="Apri" Click="OpenFaceExecutableFolder_Click" Width="56" />
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Click="OpenFaceExecutableFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,6,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings" />
<Button Grid.Column="2" Name="FaceSelectOutputButton" Content="Scegli..." Click="SelectFaceOutputFolder_Click" Width="88" />
<Button Grid.Column="3" Name="FaceOpenOutputButton" Content="Apri" Click="OpenFaceOutputFolder_Click" Width="56" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
<Button Grid.Column="3" Name="FaceOpenDestinationButton" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="File out (.pkl):" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings.pkl" />
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFile_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Name="FaceOpenOutputButton" Click="OpenFaceOutputFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Click="RunFaceEncoder_Click" />
<TextBlock Name="FaceStatusTextBlock" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
<TextBox Name="FaceOutputTextBox"
IsReadOnly="True"
AcceptsReturn="True"

View file

@ -57,7 +57,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
}
}
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
private async void SelectFaceOutputFile_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
if (outputBox is null)
@ -72,14 +72,21 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
return;
}
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
var files = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Seleziona cartella output encodings"
Title = "Seleziona file output encodings (.pkl)",
SuggestedFileName = "encodings.pkl",
DefaultExtension = "pkl",
FileTypeChoices =
[
new FilePickerFileType("Pickle file") { Patterns = ["*.pkl"] }
],
ShowOverwritePrompt = true
});
if (folders.Count > 0)
if (files is not null)
{
outputBox.Text = folders[0].Path.LocalPath;
outputBox.Text = files.Path.LocalPath;
if (DataContext is DataModel model)
{
model.FaceOutputFolderPath = outputBox.Text;
@ -119,7 +126,38 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
return;
}
OpenInExplorer(outputBox.Text);
var outputPath = outputBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(outputPath))
{
return;
}
if (File.Exists(outputPath))
{
OpenInExplorer(outputPath);
return;
}
var directory = Path.GetDirectoryName(outputPath);
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? outputPath : directory);
}
private void OpenFaceDestinationFolder_Click(object? sender, RoutedEventArgs e)
{
var destBox = this.FindControl<Avalonia.Controls.TextBox>("FaceDestinationPathTextBox");
string? path = destBox?.Text?.Trim();
if (string.IsNullOrWhiteSpace(path) && DataContext is DataModel model)
{
path = (model.DestinationPath ?? string.Empty).Trim();
}
if (string.IsNullOrWhiteSpace(path))
{
return;
}
var directory = Path.GetDirectoryName(path);
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
}
private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
@ -142,11 +180,11 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
}
var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
var outputFolder = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
var outputFilePath = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
var imagesFolder = (model.DestinationPath ?? string.Empty).Trim().Trim('"');
model.FaceExecutablePath = executablePath;
model.FaceOutputFolderPath = outputFolder;
model.FaceOutputFolderPath = outputFilePath;
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
{
@ -160,20 +198,30 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
return;
}
if (string.IsNullOrWhiteSpace(outputFolder))
if (string.IsNullOrWhiteSpace(outputFilePath))
{
statusBlock.Text = "Inserisci la cartella di output.";
statusBlock.Text = "Inserisci il file di output .pkl.";
return;
}
if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase))
{
statusBlock.Text = "Il file di output deve avere estensione .pkl.";
return;
}
try
{
Directory.CreateDirectory(outputFolder);
var outputDirectory = Path.GetDirectoryName(outputFilePath);
if (!string.IsNullOrWhiteSpace(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to create face output folder: {OutputFolder}", outputFolder);
statusBlock.Text = "Impossibile creare la cartella di output.";
_logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath);
statusBlock.Text = "Impossibile creare la cartella del file di output.";
return;
}
@ -187,7 +235,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
try
{
var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
var outputFolderArg = NormalizeDirectoryPathArgument(outputFolder);
var outputFileArg = NormalizeFilePathArgument(outputFilePath);
var processStartInfo = new ProcessStartInfo
{
@ -201,7 +249,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
processStartInfo.ArgumentList.Add("--images");
processStartInfo.ArgumentList.Add(imagesFolderArg);
processStartInfo.ArgumentList.Add("--out");
processStartInfo.ArgumentList.Add(outputFolderArg);
processStartInfo.ArgumentList.Add(outputFileArg);
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) =>
@ -314,4 +362,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
return normalized;
}
private static string NormalizeFilePathArgument(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
return value.Trim().Trim('"');
}
}

View file

@ -1,61 +1,92 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.GeneralTabView">
<ScrollViewer>
<StackPanel Margin="8">
<StackPanel Margin="4" Spacing="8">
<TextBlock Text="Percorsi" FontWeight="Bold" />
<StackPanel Margin="0,6,0,0">
<Grid Margin="0,0,0,6" ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Margin="0,2,0,0" Spacing="6">
<Grid Margin="0,0,0,2" ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenSourceFolder_Click" Content="Apri" />
<Button Width="104" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenSourceFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding DestinationPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenDestinationFolder_Click" Content="Apri" />
<Button Width="104" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenDestinationFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
</StackPanel>
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Margin="0,6,0,0">
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
</StackPanel>
<Grid ColumnDefinitions="*,*" ColumnSpacing="24" Margin="0,4,0,0">
<StackPanel Grid.Column="0" Spacing="8">
<TextBlock Text="Opzioni" FontWeight="Bold" />
<StackPanel Margin="0,2,0,0" Spacing="3">
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
</StackPanel>
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Threads:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Elaborazione" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,74,Auto,74" ColumnSpacing="8" RowDefinitions="Auto">
<TextBlock Text="Threads:" VerticalAlignment="Center" Grid.Column="0" />
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="74" Grid.Column="1" />
<TextBlock Text="Chunk:" VerticalAlignment="Center" Grid.Column="2" />
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="74" Grid.Column="3" />
</Grid>
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Libreria Immagini" FontWeight="Bold" />
<StackPanel Margin="0,2,0,0" Spacing="3">
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" IsVisible="{Binding IsRunningOnWindows}" />
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" />
</StackPanel>
</StackPanel>
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<TextBlock Text="Divisione cartelle" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,78,Auto,*" ColumnSpacing="8" RowDefinitions="Auto">
<TextBlock Text="File per cartella:" VerticalAlignment="Center" Grid.Column="0" />
<TextBox Text="{Binding FilesPerFolder}" Width="78" Grid.Column="1" />
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Grid.Column="2" />
<TextBox Text="{Binding FolderSuffix}" MinWidth="120" Grid.Column="3" />
</Grid>
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" IsVisible="{Binding IsRunningOnWindows}" />
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
</StackPanel>
<TextBlock Text="Numerazione" FontWeight="Bold" />
<StackPanel Margin="0,2,0,0" Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="12">
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" />
</StackPanel>
<Grid ColumnDefinitions="Auto,64,*" ColumnSpacing="8">
<TextBlock Text="Cifre:" VerticalAlignment="Center" Grid.Column="0" />
<TextBox Text="{Binding CounterDigits}" Width="64" Grid.Column="1" />
</Grid>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View file

@ -2,37 +2,37 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaViews.LogoTabView">
<ScrollViewer>
<StackPanel Margin="8">
<StackPanel Margin="4">
<TextBlock Text="Logo" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<Button Command="{Binding SelectLogoFileCommand}" Content="Seleziona logo" />
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center"
Width="250" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Margin="0,8,0,0">
<StackPanel Margin="0,6,0,0">
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBox Text="{Binding LogoWidth}" Width="80" />
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="6,0,0,0" />
</StackPanel>
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Margine:" VerticalAlignment="Center" />
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="6,0,0,0" />
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
<Button Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0"
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="6,0,0,0" />
<Button Command="{Binding SelectTransparentColorCommand}" Margin="6,0,0,0"
Content="Seleziona trasparente" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding HorizontalAlignments}"
SelectedItem="{Binding LogoHorizontalPosition}"
Width="120" Margin="8,0,0,0" />
Width="120" Margin="6,0,0,0" />
<ComboBox ItemsSource="{Binding VerticalPositions}"
SelectedItem="{Binding LogoVerticalPosition}"
Width="120" Margin="8,0,0,0" />
Width="120" Margin="6,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>

View file

@ -2,25 +2,25 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaViews.PhotoTabView">
<ScrollViewer>
<StackPanel Margin="8">
<StackPanel Margin="4">
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="6,0,0,0" />
</StackPanel>
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Margin="0,6,0,0">
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,6,0,0" />
<StackPanel Margin="0,4,0,0">
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
</StackPanel>
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="Qualita:" VerticalAlignment="Center" />
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="6,0,0,0" />
<TextBlock Text="Miniature Qualita:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="6,0,0,0" />
</StackPanel>
</StackPanel>
</ScrollViewer>

View file

@ -2,12 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaViews.RaceUploadTabView">
<ScrollViewer>
<StackPanel Margin="8" Spacing="8">
<StackPanel Margin="4" Spacing="6">
<TextBlock Text="Setup gara e upload foto processate" FontWeight="Bold" />
<TextBlock Text="Flusso: login admin, creazione gara, creazione punti foto, upload file processati da cartella destinazione locale, indicizzazione punti foto."
TextWrapping="Wrap" Opacity="0.8" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6" RowSpacing="6">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Text="{Binding ApiLogin, Mode=TwoWay}" Watermark="admin user" />
@ -16,7 +16,7 @@
</Grid>
<TextBlock Text="Dati gara" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="6" RowSpacing="6">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Descrizione:" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiRaceDescriptionTextBox" Text="{Binding ApiRaceDescription, Mode=TwoWay}" Watermark="Nome gara" />
@ -49,7 +49,7 @@
</ComboBox>
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6" RowSpacing="6">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Evento Omaggio:" VerticalAlignment="Center" />
<ComboBox Grid.Row="0" Grid.Column="1" Name="ApiFreeEventComboBox" SelectedIndex="{Binding ApiFreeEventIndex, Mode=TwoWay}">
<ComboBoxItem Content="0 - No" />
@ -65,12 +65,11 @@
Watermark="/percorso/remoto/foto-ridotte" />
</Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Name="ApiCreateRaceButton" Content="Crea nuova gara" Click="CreateRace_Click" />
<Button Name="ApiUploadButton" Content="Upload foto processate" Click="UploadProcessed_Click" />
<TextBlock Name="ApiStatusTextBlock" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Output" FontWeight="Bold" Margin="0,4,0,0" />
<TextBox Name="ApiOutputTextBox"
IsReadOnly="True"

View file

@ -17,18 +17,21 @@ namespace ImageCatalog_2.AvaloniaViews;
public partial class RaceUploadTabView : Avalonia.Controls.UserControl
{
private readonly IRaceUploadCommunicationClient _apiClient;
private readonly ILogger<RaceUploadTabView> _logger;
public RaceUploadTabView()
{
InitializeComponent();
_apiClient = Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
_logger = Program.ServiceProvider.GetService(typeof(ILogger<RaceUploadTabView>)) as ILogger<RaceUploadTabView>
?? NullLogger<RaceUploadTabView>.Instance;
}
private static IRaceUploadCommunicationClient CreateClient()
{
return Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
}
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
@ -76,32 +79,43 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
var saveResponse = await _apiClient.SaveRaceAsync(
new RaceSaveRequest
{
IdGara = 0,
Description = sanitizedDescription,
StartDate = startDate,
EndDate = endDate,
TipoGaraId = tipoGaraId,
EventoInLinea = model.ApiEventoInLineaIndex,
TipoIndicizzazione = model.ApiTipoIndexValue,
FreeEvent = model.ApiFreeEventIndex,
PathBase = model.ApiPathBase?.Trim(),
Localita = model.ApiLocalita?.Trim(),
},
CancellationToken.None);
RawEndpointResponse saveResponse;
RawEndpointResponse createPointsResponse;
long raceId;
var raceId = ExtractRaceId(saveResponse.Body);
if (raceId <= 0)
var client = CreateClient();
try
{
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
saveResponse = await client.SaveRaceAsync(
new RaceSaveRequest
{
IdGara = 0,
Description = sanitizedDescription,
StartDate = startDate,
EndDate = endDate,
TipoGaraId = tipoGaraId,
EventoInLinea = model.ApiEventoInLineaIndex,
TipoIndicizzazione = model.ApiTipoIndexValue,
FreeEvent = model.ApiFreeEventIndex,
PathBase = model.ApiPathBase?.Trim(),
Localita = model.ApiLocalita?.Trim(),
},
CancellationToken.None).ConfigureAwait(false);
raceId = ExtractRaceId(saveResponse.Body);
if (raceId <= 0)
{
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
}
model.ApiRaceId = raceId.ToString();
createPointsResponse = await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(false);
}
finally
{
(client as IDisposable)?.Dispose();
}
model.ApiRaceId = raceId.ToString();
var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
var sb = new StringBuilder();
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
@ -197,41 +211,50 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
var sb = new StringBuilder();
sb.AppendLine($"File da inviare: {files.Count}");
foreach (var file in files)
List<long> pointIds;
var client = CreateClient();
try
{
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
await using var stream = File.OpenRead(file);
await _apiClient.UploadFileToReceiverAsync(
new ReceiveFileUploadRequest
{
FileName = Path.GetFileName(file),
FileStream = stream,
DestinationPath = remotePath,
OverwriteRemoteFile = true,
},
CancellationToken.None).ConfigureAwait(true);
uploaded++;
if (uploaded % 20 == 0 || uploaded == files.Count)
foreach (var file in files)
{
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
await using var stream = File.OpenRead(file);
await client.UploadFileToReceiverAsync(
new ReceiveFileUploadRequest
{
FileName = Path.GetFileName(file),
FileStream = stream,
DestinationPath = remotePath,
OverwriteRemoteFile = true,
},
CancellationToken.None).ConfigureAwait(true);
uploaded++;
if (uploaded % 20 == 0 || uploaded == files.Count)
{
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
}
}
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
foreach (var pointId in pointIds)
{
await client.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
}
}
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
foreach (var pointId in pointIds)
finally
{
await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
(client as IDisposable)?.Dispose();
}
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
outputBox.Text = sb.ToString();
statusBlock.Text = "Upload e indicizzazione completati.";
@ -251,31 +274,49 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
private async Task<RawEndpointResponse> LoginAsync(string login, string password)
{
return await _apiClient.LoginAdminAsync(
new AdminLoginRequest
{
Login = login,
Password = password,
Command = "check",
},
CancellationToken.None).ConfigureAwait(false);
var client = CreateClient();
try
{
return await client.LoginAdminAsync(
new AdminLoginRequest
{
Login = login,
Password = password,
Command = "check",
},
CancellationToken.None).ConfigureAwait(false);
}
finally
{
(client as IDisposable)?.Dispose();
}
}
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
{
const int maxAttempts = 10;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
var client = CreateClient();
try
{
var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
var ids = ExtractPointIds(response.Body);
if (ids.Count > 0)
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
return ids;
var response = await client.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
var ids = ExtractPointIds(response.Body);
if (ids.Count > 0)
{
return ids;
}
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
}
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
return new List<long>();
}
finally
{
(client as IDisposable)?.Dispose();
}
return [];

View file

@ -2,52 +2,52 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaViews.TextTabView">
<ScrollViewer>
<StackPanel Margin="8">
<StackPanel Margin="4">
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,6,0,0" />
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" AcceptsReturn="True"
TextWrapping="Wrap" MinHeight="80" />
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="Font" FontWeight="Bold" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" VerticalAlignment="Center" />
<TextBox Text="{Binding FontSize}" Width="60" Margin="6,0,0,0" />
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="6,0,0,0" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Colore testo (hex)" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="Colore testo (hex)" FontWeight="Bold" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextColorRGB}" Width="120" />
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="6,0,0,0" />
</StackPanel>
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size:" VerticalAlignment="Center" />
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="6,0,0,0" />
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="6,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="6,0,0,0" />
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
<TextBox Text="{Binding TextMargin}" Width="60" Margin="6,0,0,0" />
</StackPanel>
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,12,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,8,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<CheckBox Content="Aggiungi Orario" IsChecked="{Binding AddTime}" />
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" Margin="12,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="Partenza:" VerticalAlignment="Center" />
<CalendarDatePicker SelectedDate="{Binding RaceStartDate}"
IsEnabled="{Binding AddRaceTime}"
Margin="8,0,0,0" Width="200" />
Margin="6,0,0,0" Width="200" />
<TextBox Text="{Binding TimeLabel}" Width="220" Margin="12,0,0,0" />
</StackPanel>
</StackPanel>

View file

@ -2,21 +2,21 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ImageCatalog_2.AvaloniaViews.ThumbnailsTabView">
<ScrollViewer>
<StackPanel Margin="8">
<StackPanel Margin="4">
<TextBlock Text="Miniature" FontWeight="Bold" />
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,6,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,4,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="6,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="6,0,0,0" />
</StackPanel>
<StackPanel Margin="0,8,0,0">
<StackPanel Margin="0,6,0,0">
<TextBlock Text="Modalita miniature:" VerticalAlignment="Center" />
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,6,0,0">
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,4,0,0">
<ComboBoxItem>Nessuna</ComboBoxItem>
<ComboBoxItem>Aggiungi scritta</ComboBoxItem>
<ComboBoxItem>Nome file</ComboBoxItem>

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWindowsForms>true</UseWindowsForms>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<InformationalVersion>3.0.0-alpha.63+Branch.develop.Sha.39a9baf5c618d8d79c75b89e2d5c4020939697f2</InformationalVersion>
<Version>3.0.0-alpha0063</Version>
</PropertyGroup>
<PropertyGroup>
<UpdateVersionProperties>true</UpdateVersionProperties>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<None Update="My Project\Application.myapp">
<Generator>MyApplicationCodeGenerator</Generator>
<LastGenOutput>Application.Designer.cs</LastGenOutput>
</None>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Sorgenti\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CatalogVbLib\CatalogVbLib.vbproj" />
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
<PackageReference Include="Microsoft.WindowsAPICodePack-Core" Version="1.1.0.2" />
<PackageReference Include="Microsoft.WindowsAPICodePack-Shell" Version="1.1.0.0" />
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="GitVersion.MsBuild" Version="6.0.2">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<PropertyGroup />
</Project>

View file

@ -25,6 +25,8 @@
This prevents MinVer from injecting a computed version into generated BAML/pack URIs which can cause
WPF to try loading a mismatched assembly identity at runtime. Do a full clean rebuild after this change. -->
<UpdateVersionProperties>true</UpdateVersionProperties>
<!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. -->
<MinVerSkip>true</MinVerSkip>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>embedded</DebugType>
@ -51,17 +53,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.1" />
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="AutoMapper" Version="16.1.0" />
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
<PackageReference Include="MahApps.Metro" Version="2.4.11" Condition="$([MSBuild]::IsOsPlatform('Windows'))" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.5" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" Condition="$([MSBuild]::IsOsPlatform('Windows'))" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
<PackageReference Include="Avalonia" Version="11.3.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.0" />
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -1,347 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectType>Local</ProjectType>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{8D3AA2B0-8F06-4A61-9CAD-B920EB1A8E9C}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ApplicationIcon>
</ApplicationIcon>
<AssemblyKeyContainerName>
</AssemblyKeyContainerName>
<AssemblyName>ImageCatalog</AssemblyName>
<AssemblyOriginatorKeyFile>
</AssemblyOriginatorKeyFile>
<AssemblyOriginatorKeyMode>None</AssemblyOriginatorKeyMode>
<DefaultClientScript>JScript</DefaultClientScript>
<DefaultHTMLPageLayout>Grid</DefaultHTMLPageLayout>
<DefaultTargetSchema>IE50</DefaultTargetSchema>
<DelaySign>false</DelaySign>
<OutputType>WinExe</OutputType>
<OptionCompare>Binary</OptionCompare>
<OptionExplicit>On</OptionExplicit>
<OptionStrict>On</OptionStrict>
<RootNamespace>ImageCatalog</RootNamespace>
<StartupObject>ImageCatalog.My.MyApplication</StartupObject>
<FileUpgradeFlags>
</FileUpgradeFlags>
<MyType>WindowsForms</MyType>
<OldToolsVersion>3.5</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<IsWebBootstrapper>true</IsWebBootstrapper>
<ApplicationManifest>My Project\app.manifest</ApplicationManifest>
<TargetFrameworkProfile>
</TargetFrameworkProfile>
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
<SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<PublishUrl>http://localhost/ImageCatalog/</PublishUrl>
<Install>true</Install>
<InstallFrom>Web</InstallFrom>
<UpdateEnabled>true</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.8.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>bin\</OutputPath>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<BaseAddress>285212672</BaseAddress>
<ConfigurationOverrideFile>
</ConfigurationOverrideFile>
<DefineConstants>
</DefineConstants>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<DebugSymbols>true</DebugSymbols>
<Optimize>false</Optimize>
<RegisterForComInterop>false</RegisterForComInterop>
<RemoveIntegerChecks>false</RemoveIntegerChecks>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\</OutputPath>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<BaseAddress>285212672</BaseAddress>
<ConfigurationOverrideFile>
</ConfigurationOverrideFile>
<DefineConstants>
</DefineConstants>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<DebugSymbols>false</DebugSymbols>
<Optimize>true</Optimize>
<RegisterForComInterop>false</RegisterForComInterop>
<RemoveIntegerChecks>false</RemoveIntegerChecks>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<DebugType>none</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<DebugSymbols>true</DebugSymbols>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<OutputPath>bin\x64\Debug\</OutputPath>
<BaseAddress>285212672</BaseAddress>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<DefineTrace>true</DefineTrace>
<OutputPath>bin\x64\Release\</OutputPath>
<BaseAddress>285212672</BaseAddress>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<Optimize>true</Optimize>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<OutputPath>bin\x86\Debug\</OutputPath>
<BaseAddress>285212672</BaseAddress>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<CodeAnalysisLogFile>bin\ImageCatalog.exe.CodeAnalysisLog.xml</CodeAnalysisLogFile>
<CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression>
<CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.vb</CodeAnalysisModuleSuppressionsFile>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSetDirectories>;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets</CodeAnalysisRuleSetDirectories>
<CodeAnalysisIgnoreBuiltInRuleSets>true</CodeAnalysisIgnoreBuiltInRuleSets>
<CodeAnalysisRuleDirectories>;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules</CodeAnalysisRuleDirectories>
<CodeAnalysisIgnoreBuiltInRules>true</CodeAnalysisIgnoreBuiltInRules>
<CodeAnalysisFailOnMissingRules>false</CodeAnalysisFailOnMissingRules>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<DefineTrace>true</DefineTrace>
<OutputPath>bin\x86\Release\</OutputPath>
<BaseAddress>285212672</BaseAddress>
<DocumentationFile>ImageCatalog.xml</DocumentationFile>
<Optimize>true</Optimize>
<WarningLevel>1</WarningLevel>
<NoWarn>42016,42017,42018,42019,42032,42353,42354,42355</NoWarn>
<PlatformTarget>x86</PlatformTarget>
<CodeAnalysisLogFile>bin\ImageCatalog.exe.CodeAnalysisLog.xml</CodeAnalysisLogFile>
<CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression>
<CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.vb</CodeAnalysisModuleSuppressionsFile>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSetDirectories>;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets</CodeAnalysisRuleSetDirectories>
<CodeAnalysisIgnoreBuiltInRuleSets>true</CodeAnalysisIgnoreBuiltInRuleSets>
<CodeAnalysisRuleDirectories>;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules</CodeAnalysisRuleDirectories>
<CodeAnalysisIgnoreBuiltInRules>true</CodeAnalysisIgnoreBuiltInRules>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Ben.Demystifier, Version=0.3.0.0, Culture=neutral, PublicKeyToken=a6d206e05440431a, processorArchitecture=MSIL">
<HintPath>..\packages\Ben.Demystifier.0.3.0\lib\net45\Ben.Demystifier.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualBasic.PowerPacks.Vs, Version=10.0.0.0" />
<Reference Include="System">
<Name>System</Name>
</Reference>
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Collections.Immutable.5.0.0\lib\net461\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
<Private>True</Private>
</Reference>
<Reference Include="System.Data">
<Name>System.Data</Name>
<Private>True</Private>
</Reference>
<Reference Include="System.Drawing">
<Name>System.Drawing</Name>
<Private>True</Private>
</Reference>
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Metadata, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Reflection.Metadata.5.0.0\lib\net461\System.Reflection.Metadata.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms">
<Name>System.Windows.Forms</Name>
</Reference>
<Reference Include="System.Xml">
<Name>System.XML</Name>
</Reference>
</ItemGroup>
<ItemGroup>
<Import Include="Microsoft.VisualBasic" />
<Import Include="System" />
<Import Include="System.Collections" />
<Import Include="System.Data" />
<Import Include="System.Diagnostics" />
<Import Include="System.Drawing" />
<Import Include="System.Linq" />
<Import Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.vb">
<SubType>Code</SubType>
</Compile>
<Compile Include="FileHelper.vb" />
<Compile Include="CreaImmagineSeparateMultiCore.vb" />
<Compile Include="CreaImmagineSeparateThread.vb" />
<Compile Include="ExifReader.vb">
<SubType>Code</SubType>
</Compile>
<Compile Include="Form1.vb">
<SubType>Form</SubType>
</Compile>
<Compile Include="LoadBuffer.vb" />
<Compile Include="MainForm.Designer.vb">
<DependentUpon>MainForm.vb</DependentUpon>
</Compile>
<Compile Include="MainForm.vb">
<SubType>Form</SubType>
</Compile>
<Compile Include="Module2.vb" />
<Compile Include="My Project\Application.Designer.vb">
<AutoGen>True</AutoGen>
<DependentUpon>Application.myapp</DependentUpon>
</Compile>
<Compile Include="My Project\Settings.Designer.vb">
<AutoGen>True</AutoGen>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
<Compile Include="PicSettings.vb" />
<Compile Include="XYThreadPool.vb" />
<Compile Include="Module1.vb">
<SubType>Code</SubType>
</Compile>
<Compile Include="ParametriSetup.vb">
<SubType>Code</SubType>
</Compile>
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.vb</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.vb</DependentUpon>
</EmbeddedResource>
<None Include="My Project\Application.myapp">
<Generator>MyApplicationCodeGenerator</Generator>
<LastGenOutput>Application.Designer.vb</LastGenOutput>
</None>
<None Include="app.config" />
<None Include="ClassDiagram1.cd" />
<None Include="My Project\app.manifest" />
<None Include="My Project\Settings.settings">
<CustomToolNamespace>My</CustomToolNamespace>
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.vb</LastGenOutput>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.2.0">
<Visible>False</Visible>
<ProductName>.NET Framework 2.0 %28x86%29</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.0">
<Visible>False</Visible>
<ProductName>.NET Framework 3.0 %28x86%29</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Windows.Installer.3.1">
<Visible>False</Visible>
<ProductName>Windows Installer 3.1</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Folder Include="Sorgenti\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CatalogVbLib\CatalogVbLib.vbproj">
<Project>{44465926-240d-473f-90b8-786ba4384406}</Project>
<Name>CatalogVbLib</Name>
</ProjectReference>
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj">
<Project>{aebfe9e3-277c-4a7b-8448-145d1b11998b}</Project>
<Name>MaddoShared</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="collini_canon.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="LOG_ITA.jpg" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.VisualBasic.targets" />
<PropertyGroup>
<PreBuildEvent>
</PreBuildEvent>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,7 @@
namespace ImageCatalog_2.Models;
public class AiResultItem
{
public string Path { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}

View file

@ -151,14 +151,18 @@ static class Program
var testService = sp.GetRequiredService<ITestService>();
var settingsService = sp.GetRequiredService<ISettingsService>();
var imageCreation = sp.GetRequiredService<ImageCreationService>();
var aiExtractionService = sp.GetRequiredService<IAiExtractionService>();
var imageProcessingCoordinator = sp.GetRequiredService<IImageProcessingCoordinator>();
var picSettings = sp.GetRequiredService<PicSettings>();
var mapper = sp.GetRequiredService<IMapper>();
var logger = sp.GetRequiredService<ILogger<DataModel>>();
var versionProvider = sp.GetService<MaddoShared.IVersionProvider>();
return new DataModel(testService, settingsService, imageCreation, picSettings, mapper, logger, versionProvider);
return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider);
});
services.AddTransient<IAiExtractionService, AiExtractionService>();
services.AddTransient<IImageProcessingCoordinator, ImageProcessingCoordinator>();
services.AddTransient<ImageCreationService>();
#if WINDOWS
services.AddTransient<ImageCreatorGDI>();

View file

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ImageCatalog_2.Models;
using Microsoft.Extensions.Logging;
namespace ImageCatalog_2.Services;
public class AiExtractionService : IAiExtractionService
{
private readonly ILogger<AiExtractionService> _logger;
public AiExtractionService(ILogger<AiExtractionService> logger)
{
_logger = logger;
}
public async Task RunAsync(
AiExtractionRequest request,
CancellationToken token,
Func<AiResultItem, Task> onResult,
Func<double, Task> onProgress)
{
var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var imageFiles = Directory.EnumerateFiles(request.SearchRoot, "*.*", searchOption)
.Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
.ToList();
if (imageFiles.Count == 0)
{
return;
}
var extractedResults = new List<AiResultItem>();
Type? aiProcessorType = null;
object? aiProcessor = null;
try
{
var assembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true);
if (assembly != null)
{
aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor");
if (aiProcessorType != null)
{
aiProcessor = Activator.CreateInstance(aiProcessorType);
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection");
}
var processed = 0;
var total = imageFiles.Count;
foreach (var file in imageFiles)
{
token.ThrowIfCancellationRequested();
var extracted = string.Empty;
if (aiProcessorType is not null && aiProcessor is not null)
{
try
{
var method = aiProcessorType.GetMethod("ExtractNumbersFromImage")
?? aiProcessorType.GetMethod("ExtractTextFromImage");
if (method is not null)
{
var value = method.Invoke(aiProcessor, new object[] { file });
if (value != null)
{
extracted = value.ToString() ?? string.Empty;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error invoking AI processor for {File}", file);
}
}
if (!string.IsNullOrWhiteSpace(extracted))
{
var result = new AiResultItem { Path = file, Text = extracted };
extractedResults.Add(result);
await onResult(result).ConfigureAwait(false);
}
processed++;
var percent = total > 0 ? (processed * 100.0 / total) : 100.0;
await onProgress(percent).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
{
try
{
var dir = Path.GetDirectoryName(request.CsvOutputPath) ?? string.Empty;
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
using var sw = new StreamWriter(request.CsvOutputPath, false, Encoding.UTF8);
sw.WriteLine("Path,Text");
foreach (var r in extractedResults)
{
var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
sw.WriteLine($"\"{r.Path}\",\"{safeText}\"");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath);
}
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ImageCatalog_2.Models;
namespace ImageCatalog_2.Services;
public sealed class AiExtractionRequest
{
public required string SearchRoot { get; init; }
public required bool Recursive { get; init; }
public string CsvOutputPath { get; init; } = string.Empty;
}
public interface IAiExtractionService
{
Task RunAsync(
AiExtractionRequest request,
CancellationToken token,
Func<AiResultItem, Task> onResult,
Func<double, Task> onProgress);
}

View file

@ -0,0 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MaddoShared;
namespace ImageCatalog_2.Services
{
public readonly record struct ImageProcessedUpdate(string Status, int Total, int Processed);
public sealed class ImageProcessingRunRequest
{
public required ImageCreationService.Options Options { get; init; }
}
public sealed class ImageProcessingRunResult
{
public required string FinalSpeedCounter { get; init; }
}
public interface IImageProcessingCoordinator
{
Task<ImageProcessingRunResult> RunAsync(
ImageProcessingRunRequest request,
CancellationToken token,
Action<ImageProcessedUpdate> onImageProcessed,
Action<string> onSpeedUpdated);
}
}

View file

@ -0,0 +1,127 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MaddoShared;
using Microsoft.Extensions.Logging;
namespace ImageCatalog_2.Services
{
public class ImageProcessingCoordinator : IImageProcessingCoordinator
{
private readonly ImageCreationService _imageCreationService;
private readonly ILogger<ImageProcessingCoordinator> _logger;
[CLSCompliant(false)]
public ImageProcessingCoordinator(
ImageCreationService imageCreationService,
ILogger<ImageProcessingCoordinator> logger)
{
_imageCreationService = imageCreationService;
_logger = logger;
}
public async Task<ImageProcessingRunResult> RunAsync(
ImageProcessingRunRequest request,
CancellationToken token,
Action<ImageProcessedUpdate> onImageProcessed,
Action<string> onSpeedUpdated)
{
var results = new ConcurrentBag<string>();
var recentDiffs = new Queue<int>();
const int recentWindowSize = 5;
int currentAmount = 0;
int previousAmount = 0;
int processedAtomic = 0;
var speedWatch = Stopwatch.StartNew();
using var speedTimer = new System.Threading.Timer(_ =>
{
try
{
previousAmount = currentAmount;
currentAmount = Volatile.Read(ref processedAtomic);
int diff = currentAmount - previousAmount;
if (diff < 0)
{
diff = 0;
}
lock (recentDiffs)
{
recentDiffs.Enqueue(diff);
if (recentDiffs.Count > recentWindowSize)
{
recentDiffs.Dequeue();
}
}
double avgRecent;
lock (recentDiffs)
{
avgRecent = recentDiffs.Count == 0 ? 0.0 : recentDiffs.Average();
}
double overall = 0.0;
if (speedWatch.Elapsed.TotalSeconds >= 1)
{
var elapsedSeconds = speedWatch.Elapsed.TotalSeconds;
var total = Volatile.Read(ref processedAtomic);
overall = elapsedSeconds > 0 ? total / elapsedSeconds : 0.0;
}
var recentPerMin = avgRecent * 60.0;
var elapsed = speedWatch.Elapsed;
int hours = (int)elapsed.TotalHours;
int minutes = elapsed.Minutes;
int seconds = elapsed.Seconds;
var elapsedStr = $"{hours}h {minutes}m {seconds}s";
var speedText = $"{avgRecent:0.00} f/s (media: {overall:0.00} f/s) - {elapsedStr}{Environment.NewLine}media: {recentPerMin:0.00} f/m";
onSpeedUpdated(speedText);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to update speed counter");
}
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
EventHandler<Tuple<string, int>> onImageProcessedInternal = (_, args) =>
{
var processed = Interlocked.Increment(ref processedAtomic);
onImageProcessed(new ImageProcessedUpdate(args.Item1, args.Item2, processed));
};
await _imageCreationService.CreaCatalogoParallel(
request.Options,
results,
onImageProcessedInternal,
token).ConfigureAwait(false);
speedWatch.Stop();
var finalProcessed = Volatile.Read(ref processedAtomic);
double overallAvg = 0.0;
double overallPerMin = 0.0;
if (speedWatch.Elapsed.TotalSeconds > 0.0)
{
overallAvg = finalProcessed / speedWatch.Elapsed.TotalSeconds;
overallPerMin = overallAvg * 60.0;
}
var finalElapsed = speedWatch.Elapsed;
int finalHours = (int)finalElapsed.TotalHours;
int finalMinutes = finalElapsed.Minutes;
int finalSeconds = finalElapsed.Seconds;
return new ImageProcessingRunResult
{
FinalSpeedCounter = $"{finalHours}h {finalMinutes}m {finalSeconds}s{Environment.NewLine}media: {overallAvg:0.00} f/s{Environment.NewLine}media: {overallPerMin:0.00} f/m"
};
}
}
}

View file

@ -0,0 +1,75 @@
using System.Collections.ObjectModel;
using ImageCatalog_2.Models;
namespace ImageCatalog_2.ViewModels;
public class AiSettingsViewModel : ViewModelBase
{
private bool _extractNumbers;
public bool ExtractNumbers
{
get => _extractNumbers;
set
{
_extractNumbers = value;
NotifyPropertyChanged();
}
}
private string _modelsFolderPath = string.Empty;
public string ModelsFolderPath
{
get => _modelsFolderPath;
set
{
_modelsFolderPath = value;
NotifyPropertyChanged();
}
}
private string _csvOutputPath = string.Empty;
public string CsvOutputPath
{
get => _csvOutputPath;
set
{
_csvOutputPath = value;
NotifyPropertyChanged();
}
}
private string _faceExecutablePath = string.Empty;
public string FaceExecutablePath
{
get => _faceExecutablePath;
set
{
_faceExecutablePath = value;
NotifyPropertyChanged();
}
}
private string _faceOutputFolderPath = string.Empty;
public string FaceOutputFolderPath
{
get => _faceOutputFolderPath;
set
{
_faceOutputFolderPath = value;
NotifyPropertyChanged();
}
}
private double _aiProgress;
public double AiProgress
{
get => _aiProgress;
set
{
_aiProgress = value;
NotifyPropertyChanged();
}
}
public ObservableCollection<AiResultItem> PreviewResults { get; } = new();
}

View file

@ -0,0 +1,49 @@
using System;
using System.IO;
namespace ImageCatalog_2.ViewModels;
public class PathSettingsViewModel : ViewModelBase
{
private string _sourcePath = string.Empty;
public string SourcePath
{
get => _sourcePath;
set
{
_sourcePath = value;
NotifyPropertyChanged();
}
}
private string _destinationPath = string.Empty;
public string DestinationPath
{
get => _destinationPath;
set
{
_destinationPath = value;
NotifyPropertyChanged();
}
}
public void NormalizePaths()
{
SourcePath = NormalizePath(SourcePath);
DestinationPath = NormalizePath(DestinationPath);
}
public static string NormalizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
path = path.Trim().Trim('"');
path = path.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar);
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
}
}

View file

@ -0,0 +1,82 @@
using System;
namespace ImageCatalog_2.ViewModels;
public class ProcessingStateViewModel : ViewModelBase
{
private string _processingStatus = string.Empty;
public string ProcessingStatus
{
get => _processingStatus;
set
{
_processingStatus = value;
NotifyPropertyChanged();
}
}
private int _processedImagesCount;
public int ProcessedImagesCount
{
get => _processedImagesCount;
set
{
_processedImagesCount = value;
NotifyPropertyChanged();
}
}
private int _totalImagesCount;
public int TotalImagesCount
{
get => _totalImagesCount;
set
{
_totalImagesCount = value;
NotifyPropertyChanged();
}
}
private int _progressBarValue;
public int ProgressBarValue
{
get => _progressBarValue;
set
{
_progressBarValue = value;
NotifyPropertyChanged();
}
}
private int _progressBarMaximum = 100;
public int ProgressBarMaximum
{
get => _progressBarMaximum;
set
{
_progressBarMaximum = value;
NotifyPropertyChanged();
}
}
private string _speedCounter = "-";
public string SpeedCounter
{
get => _speedCounter;
set
{
_speedCounter = value;
NotifyPropertyChanged();
}
}
public void ResetForRun()
{
ProcessingStatus = "Elaborazione in corso...";
TotalImagesCount = 0;
ProcessedImagesCount = 0;
SpeedCounter = "-f/s";
ProgressBarValue = 0;
ProgressBarMaximum = 100;
}
}

View file

@ -0,0 +1,149 @@
using System;
namespace ImageCatalog_2.ViewModels;
public class RaceUploadSettingsViewModel : ViewModelBase
{
private string _apiLogin = string.Empty;
public string ApiLogin
{
get => _apiLogin;
set
{
_apiLogin = value;
NotifyPropertyChanged();
}
}
private string _apiPassword = string.Empty;
public string ApiPassword
{
get => _apiPassword;
set
{
_apiPassword = value;
NotifyPropertyChanged();
}
}
private string _apiRaceDescription = string.Empty;
public string ApiRaceDescription
{
get => _apiRaceDescription;
set
{
_apiRaceDescription = value;
NotifyPropertyChanged();
}
}
private string _apiRaceTypeId = "1";
public string ApiRaceTypeId
{
get => _apiRaceTypeId;
set
{
_apiRaceTypeId = value;
NotifyPropertyChanged();
}
}
private DateTime _apiRaceStartDate = DateTime.Today;
public DateTime ApiRaceStartDate
{
get => _apiRaceStartDate;
set
{
_apiRaceStartDate = value;
NotifyPropertyChanged();
}
}
private DateTime _apiRaceEndDate = DateTime.Today;
public DateTime ApiRaceEndDate
{
get => _apiRaceEndDate;
set
{
_apiRaceEndDate = value;
NotifyPropertyChanged();
}
}
private string _apiPathBase = string.Empty;
public string ApiPathBase
{
get => _apiPathBase;
set
{
_apiPathBase = value;
NotifyPropertyChanged();
}
}
private string _apiLocalita = string.Empty;
public string ApiLocalita
{
get => _apiLocalita;
set
{
_apiLocalita = value;
NotifyPropertyChanged();
}
}
private int _apiEventoInLineaIndex;
public int ApiEventoInLineaIndex
{
get => _apiEventoInLineaIndex;
set
{
_apiEventoInLineaIndex = value;
NotifyPropertyChanged();
}
}
private int _apiTipoIndexValue = 1;
public int ApiTipoIndexValue
{
get => _apiTipoIndexValue;
set
{
_apiTipoIndexValue = value;
NotifyPropertyChanged();
}
}
private int _apiFreeEventIndex;
public int ApiFreeEventIndex
{
get => _apiFreeEventIndex;
set
{
_apiFreeEventIndex = value;
NotifyPropertyChanged();
}
}
private string _apiRaceId = string.Empty;
public string ApiRaceId
{
get => _apiRaceId;
set
{
_apiRaceId = value;
NotifyPropertyChanged();
}
}
private string _apiRemoteProcessedBasePath = string.Empty;
public string ApiRemoteProcessedBasePath
{
get => _apiRemoteProcessedBasePath;
set
{
_apiRemoteProcessedBasePath = value;
NotifyPropertyChanged();
}
}
}

View file

@ -0,0 +1,312 @@
namespace ImageCatalog_2.ViewModels;
public class VisualSettingsViewModel : ViewModelBase
{
private string _horizontalText = string.Empty;
public string HorizontalText
{
get => _horizontalText;
set
{
_horizontalText = value;
NotifyPropertyChanged();
}
}
private string _verticalText = string.Empty;
public string VerticalText
{
get => _verticalText;
set
{
_verticalText = value;
NotifyPropertyChanged();
}
}
private bool _overwriteImages;
public bool OverwriteImages
{
get => _overwriteImages;
set
{
_overwriteImages = value;
NotifyPropertyChanged();
}
}
private string _thumbnailPrefix = "tn_";
public string ThumbnailPrefix
{
get => _thumbnailPrefix;
set
{
_thumbnailPrefix = value;
NotifyPropertyChanged();
}
}
private int _thumbnailHeight = 350;
public int ThumbnailHeight
{
get => _thumbnailHeight;
set
{
_thumbnailHeight = value;
NotifyPropertyChanged();
}
}
private int _thumbnailWidth = 350;
public int ThumbnailWidth
{
get => _thumbnailWidth;
set
{
_thumbnailWidth = value;
NotifyPropertyChanged();
}
}
private int _photoBigHeight = 2240;
public int PhotoBigHeight
{
get => _photoBigHeight;
set
{
_photoBigHeight = value;
NotifyPropertyChanged();
}
}
private int _photoBigWidth = 2240;
public int PhotoBigWidth
{
get => _photoBigWidth;
set
{
_photoBigWidth = value;
NotifyPropertyChanged();
}
}
private int _fontSize = 20;
public int FontSize
{
get => _fontSize;
set
{
_fontSize = value;
NotifyPropertyChanged();
}
}
private int _fontSizeThumbnail = 50;
public int FontSizeThumbnail
{
get => _fontSizeThumbnail;
set
{
_fontSizeThumbnail = value;
NotifyPropertyChanged();
}
}
private string _fontName = "Arial";
public string FontName
{
get => _fontName;
set
{
_fontName = value;
NotifyPropertyChanged();
}
}
private bool _fontBold;
public bool FontBold
{
get => _fontBold;
set
{
_fontBold = value;
NotifyPropertyChanged();
}
}
private int _textTransparency;
public int TextTransparency
{
get => _textTransparency;
set
{
_textTransparency = value;
NotifyPropertyChanged();
}
}
private int _textMargin = 8;
public int TextMargin
{
get => _textMargin;
set
{
_textMargin = value;
NotifyPropertyChanged();
}
}
private string _textColorRgb = "Yellow";
public string TextColorRGB
{
get => _textColorRgb;
set
{
_textColorRgb = value;
NotifyPropertyChanged();
}
}
private string _transparentColor = "#FFFFFF";
public string TransparentColor
{
get => _transparentColor;
set
{
_transparentColor = value;
NotifyPropertyChanged();
}
}
private bool _useTransparentColor;
public bool UseTransparentColor
{
get => _useTransparentColor;
set
{
_useTransparentColor = value;
NotifyPropertyChanged();
}
}
private string _logoFile = string.Empty;
public string LogoFile
{
get => _logoFile;
set
{
_logoFile = value;
NotifyPropertyChanged();
}
}
private int _logoHeight = 430;
public int LogoHeight
{
get => _logoHeight;
set
{
_logoHeight = value;
NotifyPropertyChanged();
}
}
private int _logoWidth = 430;
public int LogoWidth
{
get => _logoWidth;
set
{
_logoWidth = value;
NotifyPropertyChanged();
}
}
private int _logoMargin = 290;
public int LogoMargin
{
get => _logoMargin;
set
{
_logoMargin = value;
NotifyPropertyChanged();
}
}
private int _logoTransparency = 100;
public int LogoTransparency
{
get => _logoTransparency;
set
{
_logoTransparency = value;
NotifyPropertyChanged();
}
}
private int _verticalTextSize = 20;
public int VerticalTextSize
{
get => _verticalTextSize;
set
{
_verticalTextSize = value;
NotifyPropertyChanged();
}
}
private int _verticalTextMargin = 6;
public int VerticalTextMargin
{
get => _verticalTextMargin;
set
{
_verticalTextMargin = value;
NotifyPropertyChanged();
}
}
private int _jpegQuality = 85;
public int JpegQuality
{
get => _jpegQuality;
set
{
_jpegQuality = value;
NotifyPropertyChanged();
}
}
private int _jpegQualityThumbnail = 30;
public int JpegQualityThumbnail
{
get => _jpegQualityThumbnail;
set
{
_jpegQualityThumbnail = value;
NotifyPropertyChanged();
}
}
private bool _addLogo;
public bool AddLogo
{
get => _addLogo;
set
{
_addLogo = value;
NotifyPropertyChanged();
}
}
private bool _keepOriginalDimensions;
public bool KeepOriginalDimensions
{
get => _keepOriginalDimensions;
set
{
_keepOriginalDimensions = value;
NotifyPropertyChanged();
}
}
}