Catalog/MaddoShared/ImageCreationStuff.cs
MaddoScientisto 73597689ed Cross-platform: remove System.Drawing deps, add #if WINDOWS
Refactored image creation APIs to use byte[] for logo data instead of System.Drawing.Image, enabling cross-platform support. Wrapped all GDI+/Windows-specific code in #if WINDOWS and updated project files to conditionally include Windows-only dependencies. Defaulted to ImageSharp on non-Windows, and updated UI and settings to reflect platform capabilities. Application now builds and runs on Linux/macOS with Avalonia and ImageSharp, while retaining full Windows functionality.
2026-02-26 19:17:23 +01:00

195 lines
No EOL
7.4 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dasync.Collections;
using Microsoft.Extensions.Logging;
namespace MaddoShared
{
public class ImageCreationService(
ILogger<ImageCreationService> logger,
PicSettings picSettings,
IImageCreator imageCreatorService)
{
public class Options
{
public bool AggiornaSottodirectory { get; set; }
public bool CreaSottocartelle { get; set; }
public int FilePerCartella { get; set; }
public string SuffissoCartelle { get; set; }
public int CifreContatore { get; set; }
public NumerazioneType NumerazioneType { get; set; }
public string SourcePath { get; set; }
public string DestinationPath { get; set; }
public int MaxThreads { get; set; }
public int ChunksSize { get; set; }
public bool LinearExecution { get; set; }
}
public async Task<string> CreaCatalogoParallel(Options options, ConcurrentBag<string> results,
EventHandler<Tuple<string, int>> updateEvent,
CancellationToken cancellationToken = default(CancellationToken))
{
var stopwatch = new Stopwatch();
stopwatch.Start();
await ProcessImagesParallel(options, results, updateEvent, cancellationToken);
stopwatch.Stop();
return
$"{stopwatch.Elapsed.Hours}h {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s ({stopwatch.Elapsed.TotalSeconds}s)";
}
/// <summary>
/// Gets the list of files that will be processed based on the provided options.
/// Useful for benchmarking and testing to understand the scope of work.
/// </summary>
public List<FileData> GetFilesToProcessPublic(Options options)
{
return GetFilesToProcess(options);
}
public async Task ProcessImagesParallel(
Options options,
ConcurrentBag<string> results,
EventHandler<Tuple<string, int>> updateEvent,
CancellationToken cancellationToken = default)
{
var dataToProcess = GetFilesToProcess(options);
// int threads = options.MaxThreads == 0 ? Environment.ProcessorCount * 2 : options.MaxThreads;
int threads = options.MaxThreads;
// Load logo once as raw bytes (cross-platform). byte[] is safe to share across threads.
byte[]? logoBytes = null;
if (picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile))
{
logoBytes = File.ReadAllBytes(picSettings.LogoNomeFile);
}
Func<FileData, Task> processFile = async fileData =>
{
var imgState = new ImageState
{
WorkFile = fileData.File,
DestDir = fileData.Directory,
};
try
{
await imageCreatorService.CreateImageAsync(imgState, logoBytes);
results.Add(fileData.File.Name);
try
{
updateEvent?.Invoke(this, new Tuple<string, int>(fileData.File.Name, dataToProcess.Count));
}
catch (Exception e)
{
logger.LogError(e, "Error in reporting update");
// Don't rethrow - continue processing other images
}
}
finally
{
// nothing to dispose — byte[] is managed
}
};
if (options.LinearExecution)
{
foreach (var fileData in dataToProcess)
await processFile(fileData);
}
else
{
var chunks = options.ChunksSize > 0
? SplitList(dataToProcess, options.ChunksSize)
: new List<List<FileData>> { dataToProcess };
foreach (var chunk in chunks)
{
await chunk.ParallelForEachAsync(
processFile,
maxDegreeOfParallelism: threads,
false,
cancellationToken);
chunk.Clear();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: false, compacting: false);
}
}
}
private List<FileData> GetFilesToProcess(Options options)
{
// Support multiple common JPEG patterns so files named .jpeg, .jpe, etc. are included
var jpgPatterns = new[] { "*.jpg", "*.jpeg", "*.jpe", "*.jfif", "*.pjpeg", "*.pjp" };
if (options.AggiornaSottodirectory && options.CreaSottocartelle)
{
var helper = new FileHelperSharp();
// Pass patterns joined by ';' - FileHelperSharp will split and handle multiple patterns
return helper.GetFilesRecursive(
new DirectoryInfo(options.SourcePath),
new DirectoryInfo(options.DestinationPath),
string.Join(";", jpgPatterns),
new FileHelperOptions
{
FilesPerFolder = options.FilePerCartella,
Suffix = options.SuffissoCartelle,
CounterSize = options.CifreContatore,
NumerationType = options.NumerazioneType
});
}
// For non-recursive or recursive enumeration without using the helper, enumerate for each pattern
var searchOption = options.AggiornaSottodirectory ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var files = jpgPatterns
.SelectMany(p => Directory.EnumerateFiles(options.SourcePath, p, searchOption))
.Distinct()
.ToList();
return files.Select(x =>
{
var fInfo = new FileInfo(x);
var filePath = fInfo.DirectoryName;
var trimmedSourcePath = options.SourcePath.TrimEnd('\\');
var newFilePath = fInfo.FullName.Replace(trimmedSourcePath, "").TrimStart('\\');
newFilePath = Path.Combine(options.DestinationPath, newFilePath);
var destFolderPath = new FileInfo(newFilePath).DirectoryName;
var destFolderInfo = new DirectoryInfo(destFolderPath);
destFolderInfo.EnsureDirectoryExists();
return new FileData(fInfo, new DirectoryInfo(new FileInfo(newFilePath).DirectoryName));
// var destDir = new FileInfo(newFilePath).Directory!;
// destDir.Create(); // Ensure exists
//
// return new FileData(fInfo, destDir);
}).ToList();
}
private static IEnumerable<List<T>> SplitList<T>(List<T> bigList, int nSize = 3)
{
for (int i = 0; i < bigList.Count; i += nSize)
{
yield return bigList.GetRange(i, Math.Min(nSize, bigList.Count - i));
}
}
}
}