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 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 CreaCatalogoParallel(Options options, ConcurrentBag results, EventHandler> 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)"; } /// /// 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. /// public List GetFilesToProcessPublic(Options options) { return GetFilesToProcess(options); } public async Task ProcessImagesParallel( Options options, ConcurrentBag results, EventHandler> 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 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(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> { 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 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> SplitList(List bigList, int nSize = 3) { for (int i = 0; i < bigList.Count; i += nSize) { yield return bigList.GetRange(i, Math.Min(nSize, bigList.Count - i)); } } } }