ResizeBatch.cs
1 #pragma warning disable IDE0073 2 // Copyright (c) Brice Lambson 3 // The Brice Lambson licenses this file to you under the MIT license. 4 // See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/ 5 #pragma warning restore IDE0073 6 7 using System; 8 using System.Collections.Concurrent; 9 using System.Collections.Generic; 10 using System.IO; 11 using System.IO.Abstractions; 12 using System.IO.Pipes; 13 using System.Linq; 14 using System.Text; 15 using System.Threading; 16 using System.Threading.Tasks; 17 18 using ImageResizer.Properties; 19 using ImageResizer.Services; 20 21 namespace ImageResizer.Models 22 { 23 public class ResizeBatch 24 { 25 private readonly IFileSystem _fileSystem = new FileSystem(); 26 private static IAISuperResolutionService _aiSuperResolutionService; 27 28 public string DestinationDirectory { get; set; } 29 30 public ICollection<string> Files { get; } = new List<string>(); 31 32 public static void SetAiSuperResolutionService(IAISuperResolutionService service) 33 { 34 _aiSuperResolutionService = service; 35 } 36 37 public static void DisposeAiSuperResolutionService() 38 { 39 _aiSuperResolutionService?.Dispose(); 40 _aiSuperResolutionService = null; 41 } 42 43 /// <summary> 44 /// Validates if a file path is a supported image format. 45 /// </summary> 46 /// <param name="path">The file path to validate.</param> 47 /// <returns>True if the path is valid and points to a supported image file.</returns> 48 private static bool IsValidImagePath(string path) 49 { 50 if (string.IsNullOrWhiteSpace(path)) 51 { 52 return false; 53 } 54 55 if (!File.Exists(path)) 56 { 57 return false; 58 } 59 60 var ext = Path.GetExtension(path)?.ToLowerInvariant(); 61 var validExtensions = new[] 62 { 63 ".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg", 64 ".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp", 65 }; 66 67 return validExtensions.Contains(ext); 68 } 69 70 /// <summary> 71 /// Creates a ResizeBatch from CliOptions. 72 /// </summary> 73 /// <param name="standardInput">Standard input stream for reading additional file paths.</param> 74 /// <param name="options">The parsed CLI options.</param> 75 /// <returns>A ResizeBatch instance.</returns> 76 public static ResizeBatch FromCliOptions(TextReader standardInput, CliOptions options) 77 { 78 var batch = new ResizeBatch 79 { 80 DestinationDirectory = options.DestinationDirectory, 81 }; 82 83 foreach (var file in options.Files) 84 { 85 // Convert relative paths to absolute paths 86 var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file); 87 if (IsValidImagePath(absolutePath)) 88 { 89 batch.Files.Add(absolutePath); 90 } 91 } 92 93 if (string.IsNullOrEmpty(options.PipeName)) 94 { 95 // NB: We read these from stdin since there are limits on the number of args you can have 96 // Only read from stdin if it's redirected (piped input), not from interactive terminal 97 string file; 98 if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In))) 99 { 100 while ((file = standardInput.ReadLine()) != null) 101 { 102 // Convert relative paths to absolute paths 103 var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file); 104 if (IsValidImagePath(absolutePath)) 105 { 106 batch.Files.Add(absolutePath); 107 } 108 } 109 } 110 } 111 else 112 { 113 using (NamedPipeClientStream pipeClient = 114 new NamedPipeClientStream(".", options.PipeName, PipeDirection.In)) 115 { 116 // Connect to the pipe or wait until the pipe is available. 117 pipeClient.Connect(); 118 119 using (StreamReader sr = new StreamReader(pipeClient, Encoding.Unicode)) 120 { 121 string file; 122 123 // Display the read text to the console 124 while ((file = sr.ReadLine()) != null) 125 { 126 if (IsValidImagePath(file)) 127 { 128 batch.Files.Add(file); 129 } 130 } 131 } 132 } 133 } 134 135 return batch; 136 } 137 138 public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args) 139 { 140 var options = CliOptions.Parse(args); 141 return FromCliOptions(standardInput, options); 142 } 143 144 public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken) 145 { 146 // NOTE: Settings.Default is captured once before parallel processing. 147 // Any changes to settings on disk during this batch will NOT be reflected until the next batch. 148 // This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch. 149 return Process(reportProgress, Settings.Default, cancellationToken); 150 } 151 152 public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken) 153 { 154 double total = Files.Count; 155 int completed = 0; 156 var errors = new ConcurrentBag<ResizeError>(); 157 158 // TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async 159 // APIs and a custom SynchronizationContext 160 Parallel.ForEach( 161 Files, 162 new ParallelOptions 163 { 164 CancellationToken = cancellationToken, 165 }, 166 (file, state, i) => 167 { 168 try 169 { 170 Execute(file, settings); 171 } 172 catch (Exception ex) 173 { 174 errors.Add(new ResizeError { File = _fileSystem.Path.GetFileName(file), Error = ex.Message }); 175 } 176 177 Interlocked.Increment(ref completed); 178 reportProgress(completed, total); 179 }); 180 181 return errors; 182 } 183 184 protected virtual void Execute(string file, Settings settings) 185 { 186 var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance; 187 new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute(); 188 } 189 } 190 }