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.
This commit is contained in:
MaddoScientisto 2026-03-12 18:48:13 +01:00
commit 3c722a66df
16 changed files with 1462 additions and 628 deletions

View file

@ -2,39 +2,53 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
<ScrollViewer>
<StackPanel Margin="4">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<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="88" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="6,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,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="88" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="6,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="88" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
Click="OpenModelsFolder_Click" Content="Apri" />
</Grid>
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
<avaloniaDataGrid:DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
AutoGenerateColumns="False" Height="200" Margin="0,4,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="88" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2" Content="Scegli..." />
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
Click="OpenCsvOutputFolder_Click" Content="Apri" />
</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>

File diff suppressed because it is too large Load diff

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>

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();
}
}
}