diff --git a/ImageSharp.sln b/ImageSharp.sln index afc7dce81..e949d1d57 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt features.md = features.md global.json = global.json ImageSharp.ruleset = ImageSharp.ruleset + ImageSharp.sln.DotSettings = ImageSharp.sln.DotSettings NuGet.config = NuGet.config build\package.json = build\package.json README.md = README.md diff --git a/Settings.StyleCop b/Settings.StyleCop index b4cc1655f..737491cb1 100644 --- a/Settings.StyleCop +++ b/Settings.StyleCop @@ -39,6 +39,8 @@ desensitivity premultiplied endianness + bootstrapper + thresholding diff --git a/src/ImageSharp/Bootstrapper.cs b/src/ImageSharp/Bootstrapper.cs index d970f6e55..f44c6568c 100644 --- a/src/ImageSharp/Bootstrapper.cs +++ b/src/ImageSharp/Bootstrapper.cs @@ -16,12 +16,12 @@ namespace ImageSharp /// /// Provides initialization code which allows extending the library. /// - public static class Bootstrapper + public class Bootstrapper { /// - /// The list of supported . + /// A lazily initialized bootstrapper default instance. /// - private static readonly List ImageFormatsList; + private static readonly Lazy Lazy = new Lazy(() => new Bootstrapper()); /// /// An object that can be used to synchronize access to the . @@ -29,41 +29,35 @@ namespace ImageSharp private static readonly object SyncRoot = new object(); /// - /// Initializes static members of the class. + /// The list of supported . /// - static Bootstrapper() - { - ImageFormatsList = new List - { - new BmpFormat(), - new JpegFormat(), - new PngFormat(), - new GifFormat() - }; - SetMaxHeaderSize(); - ParallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; - } + private readonly List imageFormatsList = new List(); + + /// + /// Gets the default instance. + /// + public static Bootstrapper Default { get; } = Lazy.Value; /// /// Gets the collection of supported /// - public static IReadOnlyCollection ImageFormats => new ReadOnlyCollection(ImageFormatsList); + public IReadOnlyCollection ImageFormats => new ReadOnlyCollection(this.imageFormatsList); /// /// Gets the global parallel options for processing tasks in parallel. /// - public static ParallelOptions ParallelOptions { get; } + public ParallelOptions ParallelOptions { get; } = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; /// /// Gets the maximum header size of all formats. /// - internal static int MaxHeaderSize { get; private set; } + internal int MaxHeaderSize { get; private set; } /// /// Adds a new to the collection of supported image formats. /// /// The new format to add. - public static void AddImageFormat(IImageFormat format) + public void AddImageFormat(IImageFormat format) { Guard.NotNull(format, nameof(format)); Guard.NotNull(format.Encoder, nameof(format), "The encoder should not be null."); @@ -72,50 +66,72 @@ namespace ImageSharp Guard.NotNullOrEmpty(format.Extension, nameof(format), "The extension should not be null or empty."); Guard.NotNullOrEmpty(format.SupportedExtensions, nameof(format), "The supported extensions not be null or empty."); - AddImageFormatLocked(format); + this.AddImageFormatLocked(format); } - private static void AddImageFormatLocked(IImageFormat format) + /// + /// Adds image format. The class is locked to make it thread safe. + /// + /// The image format. + private void AddImageFormatLocked(IImageFormat format) { lock (SyncRoot) { - GuardDuplicate(format); - - ImageFormatsList.Add(format); + if (this.GuardDuplicate(format)) + { + this.imageFormatsList.Add(format); - SetMaxHeaderSize(); + this.SetMaxHeaderSize(); + } } } - private static void GuardDuplicate(IImageFormat format) + /// + /// Checks to ensure duplicate image formats are not added. + /// + /// The image format. + /// Thrown if a duplicate is added. + /// + /// The . + /// + private bool GuardDuplicate(IImageFormat format) { if (!format.SupportedExtensions.Contains(format.Extension, StringComparer.OrdinalIgnoreCase)) { throw new ArgumentException("The supported extensions should contain the default extension.", nameof(format)); } + // ReSharper disable once ConvertClosureToMethodGroup + // Prevents method group allocation if (format.SupportedExtensions.Any(e => string.IsNullOrWhiteSpace(e))) { throw new ArgumentException("The supported extensions should not contain empty values.", nameof(format)); } - foreach (var imageFormat in ImageFormatsList) + // If there is already a format with the same extension or a format that supports that + // extension return false. + foreach (IImageFormat imageFormat in this.imageFormatsList) { if (imageFormat.Extension.Equals(format.Extension, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException("There is already a format with the same extension.", nameof(format)); + return false; } if (imageFormat.SupportedExtensions.Intersect(format.SupportedExtensions, StringComparer.OrdinalIgnoreCase).Any()) { - throw new ArgumentException("There is already a format that supports the same extension.", nameof(format)); + return false; } } + + return true; } - private static void SetMaxHeaderSize() + /// + /// Sets max header size. + /// + private void SetMaxHeaderSize() { - MaxHeaderSize = ImageFormatsList.Max(x => x.HeaderSize); + this.MaxHeaderSize = this.imageFormatsList.Max(x => x.HeaderSize); } } } diff --git a/src/ImageSharp/Formats/IImageFormat.cs b/src/ImageSharp/Formats/IImageFormat.cs index 4f28cdc80..ea2be7b29 100644 --- a/src/ImageSharp/Formats/IImageFormat.cs +++ b/src/ImageSharp/Formats/IImageFormat.cs @@ -9,6 +9,7 @@ namespace ImageSharp.Formats /// /// Encapsulates a supported image format, providing means to encode and decode an image. + /// Individual formats implements in this interface must be registered in the /// public interface IImageFormat { diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index 0813a3331..981984333 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -687,7 +687,7 @@ namespace ImageSharp.Formats Parallel.For( 0, height, - Bootstrapper.ParallelOptions, + image.Bootstrapper.ParallelOptions, y => { int yoff = this.grayImage.GetRowOffset(y); @@ -723,7 +723,7 @@ namespace ImageSharp.Formats Parallel.For( 0, height, - Bootstrapper.ParallelOptions, + image.Bootstrapper.ParallelOptions, y => { // TODO: Simplify + optimize + share duplicate code across converter methods @@ -764,7 +764,7 @@ namespace ImageSharp.Formats Parallel.For( 0, height, - Bootstrapper.ParallelOptions, + image.Bootstrapper.ParallelOptions, y => { // TODO: Simplify + optimize + share duplicate code across converter methods diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 22975a3c4..def8c575c 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -18,7 +18,11 @@ namespace ImageSharp /// /// Initializes a new instance of the class. /// - public Image() + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + public Image(Bootstrapper bootstrapper = null) + : base(bootstrapper) { } @@ -28,8 +32,11 @@ namespace ImageSharp /// /// The width of the image in pixels. /// The height of the image in pixels. - public Image(int width, int height) - : base(width, height) + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + public Image(int width, int height, Bootstrapper bootstrapper = null) + : base(width, height, bootstrapper) { } @@ -39,9 +46,12 @@ namespace ImageSharp /// /// The stream containing image information. /// + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// /// Thrown if the is null. - public Image(Stream stream) - : base(stream) + public Image(Stream stream, Bootstrapper bootstrapper = null) + : base(stream, bootstrapper) { } @@ -51,9 +61,12 @@ namespace ImageSharp /// /// The byte array containing image information. /// + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// /// Thrown if the is null. - public Image(byte[] bytes) - : base(bytes) + public Image(byte[] bytes, Bootstrapper bootstrapper = null) + : base(bytes, bootstrapper) { } diff --git a/src/ImageSharp/Image/ImageBase{TColor}.cs b/src/ImageSharp/Image/ImageBase{TColor}.cs index 8fa40facc..4ac847d16 100644 --- a/src/ImageSharp/Image/ImageBase{TColor}.cs +++ b/src/ImageSharp/Image/ImageBase{TColor}.cs @@ -25,8 +25,12 @@ namespace ImageSharp /// /// Initializes a new instance of the class. /// - protected ImageBase() + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + protected ImageBase(Bootstrapper bootstrapper = null) { + this.Bootstrapper = bootstrapper ?? Bootstrapper.Default; } /// @@ -34,11 +38,15 @@ namespace ImageSharp /// /// The width of the image in pixels. /// The height of the image in pixels. + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// /// /// Thrown if either or are less than or equal to 0. /// - protected ImageBase(int width, int height) + protected ImageBase(int width, int height, Bootstrapper bootstrapper = null) { + this.Bootstrapper = bootstrapper ?? Bootstrapper.Default; this.InitPixels(width, height); } @@ -95,6 +103,11 @@ namespace ImageSharp /// public int FrameDelay { get; set; } + /// + /// Gets the bootstrapper providing initialization code which allows extending the library. + /// + public Bootstrapper Bootstrapper { get; private set; } + /// public void InitPixels(int width, int height) { @@ -157,8 +170,9 @@ namespace ImageSharp /// protected void CopyProperties(ImageBase other) { + this.Bootstrapper = other.Bootstrapper; this.Quality = other.Quality; this.FrameDelay = other.FrameDelay; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Image/ImageFrame{TColor}.cs b/src/ImageSharp/Image/ImageFrame{TColor}.cs index 4c67bd968..fe064c244 100644 --- a/src/ImageSharp/Image/ImageFrame{TColor}.cs +++ b/src/ImageSharp/Image/ImageFrame{TColor}.cs @@ -19,7 +19,11 @@ namespace ImageSharp /// /// Initializes a new instance of the class. /// - public ImageFrame() + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + public ImageFrame(Bootstrapper bootstrapper = null) + : base(bootstrapper) { } @@ -49,7 +53,7 @@ namespace ImageSharp { scaleFunc = PackedPixelConverterHelper.ComputeScaleFunction(scaleFunc); - ImageFrame target = new ImageFrame + ImageFrame target = new ImageFrame(this.Bootstrapper) { Quality = this.Quality, FrameDelay = this.FrameDelay @@ -63,7 +67,7 @@ namespace ImageSharp Parallel.For( 0, target.Height, - Bootstrapper.ParallelOptions, + this.Bootstrapper.ParallelOptions, y => { for (int x = 0; x < target.Width; x++) diff --git a/src/ImageSharp/Image/ImageProcessingExtensions.cs b/src/ImageSharp/Image/ImageProcessingExtensions.cs index 16ce455d2..0b6961b12 100644 --- a/src/ImageSharp/Image/ImageProcessingExtensions.cs +++ b/src/ImageSharp/Image/ImageProcessingExtensions.cs @@ -41,6 +41,11 @@ namespace ImageSharp internal static Image Process(this Image source, Rectangle sourceRectangle, IImageFilteringProcessor processor) where TColor : struct, IPackedPixel, IEquatable { + if (processor.ParallelOptions == null) + { + processor.ParallelOptions = source.Bootstrapper.ParallelOptions; + } + return PerformAction(source, (sourceImage) => processor.Apply(sourceImage, sourceRectangle)); } diff --git a/src/ImageSharp/Image/Image{TColor}.cs b/src/ImageSharp/Image/Image{TColor}.cs index 63e73ed3e..c60585ab3 100644 --- a/src/ImageSharp/Image/Image{TColor}.cs +++ b/src/ImageSharp/Image/Image{TColor}.cs @@ -40,9 +40,14 @@ namespace ImageSharp /// /// Initializes a new instance of the class. /// - public Image() + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + public Image(Bootstrapper bootstrapper = null) + : base(bootstrapper) { - this.CurrentImageFormat = Bootstrapper.ImageFormats.First(f => f.GetType() == typeof(PngFormat)); + // We want to throw here. + this.CurrentImageFormat = this.Bootstrapper.ImageFormats.First(); } /// @@ -51,10 +56,13 @@ namespace ImageSharp /// /// The width of the image in pixels. /// The height of the image in pixels. - public Image(int width, int height) - : base(width, height) + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// + public Image(int width, int height, Bootstrapper bootstrapper = null) + : base(width, height, bootstrapper) { - this.CurrentImageFormat = Bootstrapper.ImageFormats.First(f => f.GetType() == typeof(PngFormat)); + this.CurrentImageFormat = this.Bootstrapper.ImageFormats.First(); } /// @@ -63,8 +71,12 @@ namespace ImageSharp /// /// The stream containing image information. /// + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// /// Thrown if the is null. - public Image(Stream stream) + public Image(Stream stream, Bootstrapper bootstrapper = null) + : base(bootstrapper) { Guard.NotNull(stream, nameof(stream)); this.Load(stream); @@ -76,8 +88,12 @@ namespace ImageSharp /// /// The byte array containing image information. /// + /// + /// The bootstrapper providing initialization code which allows extending the library. + /// /// Thrown if the is null. - public Image(byte[] bytes) + public Image(byte[] bytes, Bootstrapper bootstrapper = null) + : base(bootstrapper) { Guard.NotNull(bytes, nameof(bytes)); @@ -293,7 +309,7 @@ namespace ImageSharp { scaleFunc = PackedPixelConverterHelper.ComputeScaleFunction(scaleFunc); - Image target = new Image(this.Width, this.Height) + Image target = new Image(this.Width, this.Height, this.Bootstrapper) { Quality = this.Quality, FrameDelay = this.FrameDelay, @@ -309,7 +325,7 @@ namespace ImageSharp Parallel.For( 0, target.Height, - Bootstrapper.ParallelOptions, + this.Bootstrapper.ParallelOptions, y => { for (int x = 0; x < target.Width; x++) @@ -373,7 +389,7 @@ namespace ImageSharp /// private void Load(Stream stream) { - if (!Bootstrapper.ImageFormats.Any()) + if (!this.Bootstrapper.ImageFormats.Any()) { return; } @@ -408,7 +424,7 @@ namespace ImageSharp StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Image cannot be loaded. Available formats:"); - foreach (IImageFormat format in Bootstrapper.ImageFormats) + foreach (IImageFormat format in this.Bootstrapper.ImageFormats) { stringBuilder.AppendLine("-" + format); } @@ -425,20 +441,20 @@ namespace ImageSharp /// private bool Decode(Stream stream) { - int maxHeaderSize = Bootstrapper.MaxHeaderSize; + int maxHeaderSize = this.Bootstrapper.MaxHeaderSize; if (maxHeaderSize <= 0) { return false; } - IImageFormat format = null; + IImageFormat format; byte[] header = ArrayPool.Shared.Rent(maxHeaderSize); try { long startPosition = stream.Position; stream.Read(header, 0, maxHeaderSize); stream.Position = startPosition; - format = Bootstrapper.ImageFormats.FirstOrDefault(x => x.IsSupportedFileFormat(header)); + format = this.Bootstrapper.ImageFormats.FirstOrDefault(x => x.IsSupportedFileFormat(header)); } finally { @@ -455,4 +471,4 @@ namespace ImageSharp return true; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/ImageProcessor.cs b/src/ImageSharp/ImageProcessor.cs index 46fcb574d..72d9f6b93 100644 --- a/src/ImageSharp/ImageProcessor.cs +++ b/src/ImageSharp/ImageProcessor.cs @@ -16,7 +16,7 @@ namespace ImageSharp.Processors where TColor : struct, IPackedPixel, IEquatable { /// - public virtual ParallelOptions ParallelOptions { get; set; } = Bootstrapper.ParallelOptions; + public virtual ParallelOptions ParallelOptions { get; set; } /// public virtual bool Compand { get; set; } = false; diff --git a/src/ImageSharp/Quantizers/QuantizedImage.cs b/src/ImageSharp/Quantizers/QuantizedImage.cs index 5ce53e71c..6b78ce801 100644 --- a/src/ImageSharp/Quantizers/QuantizedImage.cs +++ b/src/ImageSharp/Quantizers/QuantizedImage.cs @@ -71,16 +71,16 @@ namespace ImageSharp.Quantizers Image image = new Image(); int pixelCount = this.Pixels.Length; - int palletCount = this.Palette.Length - 1; + int palleteCount = this.Palette.Length - 1; TColor[] pixels = new TColor[pixelCount]; Parallel.For( 0, pixelCount, - Bootstrapper.ParallelOptions, + image.Bootstrapper.ParallelOptions, i => { - TColor color = this.Palette[Math.Min(palletCount, this.Pixels[i])]; + TColor color = this.Palette[Math.Min(palleteCount, this.Pixels[i])]; pixels[i] = color; }); diff --git a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs b/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs index e235e68e8..3035b1098 100644 --- a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs +++ b/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs @@ -773,7 +773,7 @@ namespace ImageSharp.Quantizers Parallel.For( 0, height, - Bootstrapper.ParallelOptions, + Bootstrapper.Default.ParallelOptions, y => { byte[] rgba = ArrayPool.Shared.Rent(4); diff --git a/tests/ImageSharp.Benchmarks/Image/CopyPixels.cs b/tests/ImageSharp.Benchmarks/Image/CopyPixels.cs index 3c2b1b5d2..b17dc7aaf 100644 --- a/tests/ImageSharp.Benchmarks/Image/CopyPixels.cs +++ b/tests/ImageSharp.Benchmarks/Image/CopyPixels.cs @@ -25,7 +25,7 @@ namespace ImageSharp.Benchmarks.Image Parallel.For( 0, source.Height, - Bootstrapper.ParallelOptions, + Bootstrapper.Default.ParallelOptions, y => { for (int x = 0; x < source.Width; x++) diff --git a/tests/ImageSharp.Tests/BootstrapperTests.cs b/tests/ImageSharp.Tests/BootstrapperTests.cs index b07fc01da..38d2c1150 100644 --- a/tests/ImageSharp.Tests/BootstrapperTests.cs +++ b/tests/ImageSharp.Tests/BootstrapperTests.cs @@ -52,7 +52,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(null); + Bootstrapper.Default.AddImageFormat(null); }); var format = new TestFormat(); @@ -60,7 +60,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("decoder", exception.Message); @@ -69,7 +69,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("encoder", exception.Message); @@ -78,7 +78,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("mime type", exception.Message); @@ -87,7 +87,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("mime type", exception.Message); @@ -96,7 +96,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("extension", exception.Message); @@ -105,7 +105,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("extension", exception.Message); @@ -114,7 +114,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("supported extensions", exception.Message); @@ -123,7 +123,7 @@ namespace ImageSharp.Tests exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("supported extensions", exception.Message); } @@ -131,32 +131,32 @@ namespace ImageSharp.Tests [Fact] public void AddImageFormatChecks() { - var format = new TestFormat(); + TestFormat format = new TestFormat(); var exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("format with the same", exception.Message); format.Extension = "test"; exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("should contain", exception.Message); format.SupportedExtensions = new string[] { "test", "jpg" }; exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("supports the same", exception.Message); format.SupportedExtensions = new string[] { "test", "" }; exception = Assert.Throws(() => { - Bootstrapper.AddImageFormat(format); + Bootstrapper.Default.AddImageFormat(format); }); Assert.Contains("empty values", exception.Message); } diff --git a/tests/ImageSharp.Tests/TestBase.cs b/tests/ImageSharp.Tests/TestBase.cs index 91c7adef1..a7732972e 100644 --- a/tests/ImageSharp.Tests/TestBase.cs +++ b/tests/ImageSharp.Tests/TestBase.cs @@ -7,11 +7,33 @@ namespace ImageSharp.Tests { using System.IO; + using ImageSharp.Formats; + /// - /// The test base class. + /// The test base class. Inherit from this class for any image manipulation tests. /// public abstract class TestBase { + /// + /// Initializes a new instance of the class. + /// + protected TestBase() + { + // Register the individual image formats. + Bootstrapper.Default.AddImageFormat(new PngFormat()); + Bootstrapper.Default.AddImageFormat(new JpegFormat()); + Bootstrapper.Default.AddImageFormat(new BmpFormat()); + Bootstrapper.Default.AddImageFormat(new GifFormat()); + } + + /// + /// Creates the image output directory. + /// + /// The path. + /// The path parts. + /// + /// The . + /// protected string CreateOutputDirectory(string path, params string[] pathParts) { path = Path.Combine("TestOutput", path); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index ad8cfe157..5fc9f175e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -96,7 +96,7 @@ namespace ImageSharp.Tests private static IImageFormat GetImageFormatByExtension(string extension) { extension = extension.ToLower(); - return Bootstrapper.ImageFormats.First(f => f.SupportedExtensions.Contains(extension)); + return Bootstrapper.Default.ImageFormats.First(f => f.SupportedExtensions.Contains(extension)); } private string GetTestOutputDir()