From 53fc42d1089ac1600ddf01af72c591d093d70b68 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 19 Jun 2023 22:00:44 +0200 Subject: [PATCH 01/11] End each row on a byte boundary --- src/ImageSharp/Formats/Pbm/BinaryEncoder.cs | 34 +++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index b179c775cf..ca1e4eaa2e 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -33,10 +33,14 @@ internal class BinaryEncoder { WriteGrayscale(configuration, stream, image); } - else + else if (componentType == PbmComponentType.Short) { WriteWideGrayscale(configuration, stream, image); } + else + { + throw new ImageFormatException("Component type not supported for Grayscale PBM."); + } } else if (colorType == PbmColorType.Rgb) { @@ -44,14 +48,25 @@ internal class BinaryEncoder { WriteRgb(configuration, stream, image); } - else + else if (componentType == PbmComponentType.Short) { WriteWideRgb(configuration, stream, image); } + else + { + throw new ImageFormatException("Component type not supported for Color PBM."); + } } else { - WriteBlackAndWhite(configuration, stream, image); + if (componentType == PbmComponentType.Bit) + { + WriteBlackAndWhite(configuration, stream, image); + } + else + { + throw new ImageFormatException("Component type not supported for Black & White PBM."); + } } } @@ -165,7 +180,6 @@ internal class BinaryEncoder Span rowSpan = row.GetSpan(); int previousValue = 0; - int startBit = 0; for (int y = 0; y < height; y++) { Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); @@ -178,7 +192,7 @@ internal class BinaryEncoder for (int x = 0; x < width;) { int value = previousValue; - for (int i = startBit; i < 8; i++) + for (int i = 0; i < 8; i++) { if (rowSpan[x].PackedValue < 128) { @@ -186,19 +200,15 @@ internal class BinaryEncoder } x++; + // End each row on a byte boundary. if (x == width) { - previousValue = value; - startBit = (i + 1) & 7; // Round off to below 8. break; } } - if (startBit == 0) - { - stream.WriteByte((byte)value); - previousValue = 0; - } + stream.WriteByte((byte)value); + previousValue = 0; } } } From bbc14f4ff529925a9e937168cc1ad54a6489c03e Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 21 Jun 2023 18:48:19 +0200 Subject: [PATCH 02/11] Row boundary on decode --- src/ImageSharp/Formats/Pbm/BinaryDecoder.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index d49633575d..449548f3d2 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -152,7 +152,6 @@ internal class BinaryDecoder { int width = pixels.Width; int height = pixels.Height; - int startBit = 0; MemoryAllocator allocator = configuration.MemoryAllocator; using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); @@ -162,23 +161,11 @@ internal class BinaryDecoder for (int x = 0; x < width;) { int raw = stream.ReadByte(); - int bit = startBit; - startBit = 0; - for (; bit < 8; bit++) + for (int bit = 0; bit < 8; bit++) { bool bitValue = (raw & (0x80 >> bit)) != 0; rowSpan[x] = bitValue ? black : white; x++; - if (x == width) - { - startBit = (bit + 1) & 7; // Round off to below 8. - if (startBit != 0) - { - stream.Seek(-1, System.IO.SeekOrigin.Current); - } - - break; - } } } From 7e4825332b685992e8e965ed61b703125b7bdde7 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 21 Jun 2023 19:14:21 +0200 Subject: [PATCH 03/11] Out of range fix --- src/ImageSharp/Formats/Pbm/BinaryDecoder.cs | 3 ++- src/ImageSharp/Formats/Pbm/BinaryEncoder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index 449548f3d2..f629282340 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -161,7 +161,8 @@ internal class BinaryDecoder for (int x = 0; x < width;) { int raw = stream.ReadByte(); - for (int bit = 0; bit < 8; bit++) + int stopBit = Math.Min(8, width - x); + for (int bit = 0; bit < stopBit; bit++) { bool bitValue = (raw & (0x80 >> bit)) != 0; rowSpan[x] = bitValue ? black : white; diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index ca1e4eaa2e..b9bc812f0d 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -192,7 +192,8 @@ internal class BinaryEncoder for (int x = 0; x < width;) { int value = previousValue; - for (int i = 0; i < 8; i++) + int stopBit = Math.Min(8, width - x); + for (int i = 0; i < stopBit; i++) { if (rowSpan[x].PackedValue < 128) { From c60cae82acbaf58e16a03869c2c1ddd9689d8ddc Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 21 Jun 2023 19:14:32 +0200 Subject: [PATCH 04/11] Tests --- tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs | 1 + tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs | 6 ++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../PbmDecoderTests/DecodeReferenceImage_L8_issue2477.png | 3 +++ tests/Images/Input/Pbm/issue2477.pbm | 3 +++ 5 files changed, 14 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/PbmDecoderTests/DecodeReferenceImage_L8_issue2477.png create mode 100644 tests/Images/Input/Pbm/issue2477.pbm diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 5bdab7b37a..1b57663f3a 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -81,6 +81,7 @@ public class PbmDecoderTests [Theory] [WithFile(BlackAndWhitePlain, PixelTypes.L8, "pbm")] [WithFile(BlackAndWhiteBinary, PixelTypes.L8, "pbm")] + [WithFile(Issue2477, PixelTypes.L8, "pbm")] [WithFile(GrayscalePlain, PixelTypes.L8, "pgm")] [WithFile(GrayscalePlainNormalized, PixelTypes.L8, "pgm")] [WithFile(GrayscaleBinary, PixelTypes.L8, "pgm")] diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs index a0a4c1164f..05f1d963b2 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs @@ -26,6 +26,7 @@ public class PbmEncoderTests { { BlackAndWhiteBinary, PbmColorType.BlackAndWhite }, { BlackAndWhitePlain, PbmColorType.BlackAndWhite }, + { Issue2477, PbmColorType.BlackAndWhite }, { GrayscaleBinary, PbmColorType.Grayscale }, { GrayscaleBinaryWide, PbmColorType.Grayscale }, { GrayscalePlain, PbmColorType.Grayscale }, @@ -96,6 +97,11 @@ public class PbmEncoderTests public void PbmEncoder_P4_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestPbmEncoderCore(provider, PbmColorType.BlackAndWhite, PbmEncoding.Binary); + [Theory] + [WithFile(Issue2477, PixelTypes.Rgb24)] + public void PbmEncoder_P4_Irregular_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestPbmEncoderCore(provider, PbmColorType.BlackAndWhite, PbmEncoding.Binary); + [Theory] [WithFile(GrayscalePlainMagick, PixelTypes.Rgb24)] public void PbmEncoder_P2_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 39b8c95a9c..c4eb866c7c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1038,5 +1038,6 @@ public static class TestImages public const string RgbPlain = "Pbm/rgb_plain.ppm"; public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; + public const string Issue2477 = "Pbm/issue2477.pbm"; } } diff --git a/tests/Images/External/ReferenceOutput/PbmDecoderTests/DecodeReferenceImage_L8_issue2477.png b/tests/Images/External/ReferenceOutput/PbmDecoderTests/DecodeReferenceImage_L8_issue2477.png new file mode 100644 index 0000000000..e8a70e6b41 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PbmDecoderTests/DecodeReferenceImage_L8_issue2477.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:670bc844ba878afa0f03574dea23ab774ac0cc5aa371d0f4b4dff7da4d32f916 +size 2912 diff --git a/tests/Images/Input/Pbm/issue2477.pbm b/tests/Images/Input/Pbm/issue2477.pbm new file mode 100644 index 0000000000..0123c65ee2 --- /dev/null +++ b/tests/Images/Input/Pbm/issue2477.pbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d625635f7be760fbea935056c0f6d046832dd74bba33a1597b52ab3dfe0c5e4e +size 4956 From eab151fada473198f4f722ec2384677ba917b573 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 21 Jun 2023 19:20:45 +0200 Subject: [PATCH 05/11] Style fix --- src/ImageSharp/Formats/Pbm/BinaryEncoder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index b9bc812f0d..abb6d2fca1 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -201,6 +201,7 @@ internal class BinaryEncoder } x++; + // End each row on a byte boundary. if (x == width) { From 4c70682d09f26e29768a53786dce4df98823b8b0 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 21 Jun 2023 22:33:08 +0200 Subject: [PATCH 06/11] Simplify code --- src/ImageSharp/Formats/Pbm/BinaryEncoder.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index abb6d2fca1..dddc629b3e 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -179,7 +179,6 @@ internal class BinaryEncoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); - int previousValue = 0; for (int y = 0; y < height; y++) { Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); @@ -191,7 +190,7 @@ internal class BinaryEncoder for (int x = 0; x < width;) { - int value = previousValue; + int value = 0; int stopBit = Math.Min(8, width - x); for (int i = 0; i < stopBit; i++) { @@ -201,16 +200,9 @@ internal class BinaryEncoder } x++; - - // End each row on a byte boundary. - if (x == width) - { - break; - } } stream.WriteByte((byte)value); - previousValue = 0; } } } From 6c439092fd5b89dfddd6b67e13be5c869652a0ad Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 24 Jun 2023 16:09:29 +0200 Subject: [PATCH 07/11] If we read a extension code, read the next 5 bits because some encoder write premature EOL codes instead of extension codes. Throw NotSupportedException, if its actually a Extension code. Fixes #2451 --- .../Compression/Decompressors/T6BitReader.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs index 8be0939d3a..fe71b204b1 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs @@ -125,13 +125,29 @@ internal sealed class T6BitReader : T4BitReader if (value == Len7Code0000000.Code) { this.Code = Len7Code0000000; - return false; + + // We do not support Extensions1D codes, but some encoders (scanner from epson) write a premature EOL code, + // which at this point cannot be distinguished from a distinguish, because we read the data bit by bit. + // Read the next 5 bit, if its a EOL code return true, indicating its the end of the image. + if (this.ReadValue(5) == 1) + { + return true; + } + + throw new NotSupportedException("ccitt extensions 1D codes are not supported."); } if (value == Len7Code0000001.Code) { this.Code = Len7Code0000001; - return false; + + // Same as above, we do not support Extensions2D codes, but it could be a EOL instead. + if (this.ReadValue(5) == 1) + { + return true; + } + + throw new NotSupportedException("ccitt extensions 2D codes are not supported."); } if (value == Len7Code0000011.Code) From 63aece9018cbf6fcaf5b794998da4d87f4dcaa6a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 25 Jun 2023 14:47:26 +1000 Subject: [PATCH 08/11] Update src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs --- .../Formats/Tiff/Compression/Decompressors/T6BitReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs index fe71b204b1..ea03094878 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs @@ -127,7 +127,7 @@ internal sealed class T6BitReader : T4BitReader this.Code = Len7Code0000000; // We do not support Extensions1D codes, but some encoders (scanner from epson) write a premature EOL code, - // which at this point cannot be distinguished from a distinguish, because we read the data bit by bit. + // which at this point cannot be distinguished from the marker, because we read the data bit by bit. // Read the next 5 bit, if its a EOL code return true, indicating its the end of the image. if (this.ReadValue(5) == 1) { From 5b1873ee9a19c9312a4c4d056481e1b17e40744b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 13:12:39 +0200 Subject: [PATCH 09/11] Use `FileOptions.Asynchronous` when doing async IO --- src/ImageSharp/IO/IFileSystem.cs | 26 +++++- src/ImageSharp/IO/LocalFileSystem.cs | 20 +++- src/ImageSharp/Image.FromFile.cs | 8 +- src/ImageSharp/ImageExtensions.cs | 2 +- .../IO/LocalFileSystemTests.cs | 93 +++++++++++++++---- .../ImageSharp.Tests/Image/ImageSaveTests.cs | 8 +- .../Image/ImageTests.ImageLoadTestBase.cs | 6 ++ tests/ImageSharp.Tests/TestFileSystem.cs | 44 +++++---- .../TestUtilities/SingleStreamFileSystem.cs | 4 + 9 files changed, 161 insertions(+), 50 deletions(-) diff --git a/src/ImageSharp/IO/IFileSystem.cs b/src/ImageSharp/IO/IFileSystem.cs index 96a9b5ba01..0f5113eff4 100644 --- a/src/ImageSharp/IO/IFileSystem.cs +++ b/src/ImageSharp/IO/IFileSystem.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.IO; @@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO; internal interface IFileSystem { /// - /// Returns a readable stream as defined by the path. + /// Opens a file as defined by the path and returns it as a readable stream. /// /// Path to the file to open. - /// A stream representing the file to open. + /// A stream representing the opened file. Stream OpenRead(string path); /// - /// Creates or opens a file and returns it as a writable stream as defined by the path. + /// Opens a file as defined by the path and returns it as a readable stream + /// that can be used for asynchronous reading. /// /// Path to the file to open. - /// A stream representing the file to open. + /// A stream representing the opened file. + Stream OpenReadAsynchronous(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream. + /// + /// Path to the file to open. + /// A stream representing the opened file. Stream Create(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream + /// that can be used for asynchronous reading and writing. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream CreateAsynchronous(string path); } diff --git a/src/ImageSharp/IO/LocalFileSystem.cs b/src/ImageSharp/IO/LocalFileSystem.cs index f4dfa2fe14..d1f619f486 100644 --- a/src/ImageSharp/IO/LocalFileSystem.cs +++ b/src/ImageSharp/IO/LocalFileSystem.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.IO; @@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem /// public Stream OpenRead(string path) => File.OpenRead(path); + /// + public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous, + }); + /// public Stream Create(string path) => File.Create(path); + + /// + public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + }); } diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index 884acf7a40..a20e5d6c58 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -72,7 +72,7 @@ public abstract partial class Image { Guard.NotNull(options, nameof(options)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -144,7 +144,7 @@ public abstract partial class Image CancellationToken cancellationToken = default) { Guard.NotNull(options, nameof(options)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -214,7 +214,7 @@ public abstract partial class Image string path, CancellationToken cancellationToken = default) { - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -291,7 +291,7 @@ public abstract partial class Image Guard.NotNull(options, nameof(options)); Guard.NotNull(path, nameof(path)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index cf970b3166..75e4f13257 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -70,7 +70,7 @@ public static partial class ImageExtensions Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using Stream fs = source.GetConfiguration().FileSystem.Create(path); + await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path); await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); } diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 3d512b7d27..30818f999a 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.IO; @@ -11,36 +11,97 @@ public class LocalFileSystemTests public void OpenRead() { string path = Path.GetTempFileName(); - string testData = Guid.NewGuid().ToString(); - File.WriteAllText(path, testData); + try + { + string testData = Guid.NewGuid().ToString(); + File.WriteAllText(path, testData); - var fs = new LocalFileSystem(); + LocalFileSystem fs = new(); - using (var r = new StreamReader(fs.OpenRead(path))) - { - string data = r.ReadToEnd(); + using (Stream stream = fs.OpenRead(path)) + using (StreamReader reader = new(stream)) + { + string data = reader.ReadToEnd(); - Assert.Equal(testData, data); + Assert.Equal(testData, data); + } + } + finally + { + File.Delete(path); } + } - File.Delete(path); + [Fact] + public async Task OpenReadAsynchronous() + { + string path = Path.GetTempFileName(); + try + { + string testData = Guid.NewGuid().ToString(); + File.WriteAllText(path, testData); + + LocalFileSystem fs = new(); + + await using (Stream stream = fs.OpenReadAsynchronous(path)) + using (StreamReader reader = new(stream)) + { + string data = await reader.ReadToEndAsync(); + + Assert.Equal(testData, data); + } + } + finally + { + File.Delete(path); + } } [Fact] public void Create() { string path = Path.GetTempFileName(); - string testData = Guid.NewGuid().ToString(); - var fs = new LocalFileSystem(); + try + { + string testData = Guid.NewGuid().ToString(); + LocalFileSystem fs = new(); - using (var r = new StreamWriter(fs.Create(path))) + using (Stream stream = fs.Create(path)) + using (StreamWriter writer = new(stream)) + { + writer.Write(testData); + } + + string data = File.ReadAllText(path); + Assert.Equal(testData, data); + } + finally { - r.Write(testData); + File.Delete(path); } + } - string data = File.ReadAllText(path); - Assert.Equal(testData, data); + [Fact] + public async Task CreateAsynchronous() + { + string path = Path.GetTempFileName(); + try + { + string testData = Guid.NewGuid().ToString(); + LocalFileSystem fs = new(); - File.Delete(path); + await using (Stream stream = fs.CreateAsynchronous(path)) + using (StreamWriter writer = new(stream)) + { + await writer.WriteAsync(testData); + } + + string data = File.ReadAllText(path); + Assert.Equal(testData, data); + } + finally + { + File.Delete(path); + } } } diff --git a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs index a3f03bed5a..f9c01ab564 100644 --- a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs @@ -44,7 +44,7 @@ public class ImageSaveTests : IDisposable [Fact] public void SavePath() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream); this.image.Save("path.png"); @@ -54,7 +54,7 @@ public class ImageSaveTests : IDisposable [Fact] public void SavePathWithEncoder() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream); this.image.Save("path.jpg", this.encoderNotInFormat.Object); @@ -73,7 +73,7 @@ public class ImageSaveTests : IDisposable [Fact] public void SaveStreamWithMime() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.image.Save(stream, this.localImageFormat.Object); this.encoder.Verify(x => x.Encode(this.image, stream)); @@ -82,7 +82,7 @@ public class ImageSaveTests : IDisposable [Fact] public void SaveStreamWithEncoder() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.image.Save(stream, this.encoderNotInFormat.Object); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index a8f9981b44..996310d8c3 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -122,6 +122,7 @@ public partial class ImageTests Stream StreamFactory() => this.DataStream; this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory); + this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory); this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory); this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object; this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem; @@ -132,6 +133,11 @@ public partial class ImageTests // Clean up the global object; this.localStreamReturnImageRgba32?.Dispose(); this.localStreamReturnImageAgnostic?.Dispose(); + + if (this.dataStreamLazy.IsValueCreated) + { + this.dataStreamLazy.Value.Dispose(); + } } protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker); diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs index 8aefbe320e..9013d15530 100644 --- a/tests/ImageSharp.Tests/TestFileSystem.cs +++ b/tests/ImageSharp.Tests/TestFileSystem.cs @@ -1,6 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#nullable enable + namespace SixLabors.ImageSharp.Tests; /// @@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests; /// public class TestFileSystem : ImageSharp.IO.IFileSystem { - private readonly Dictionary> fileSystem = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> fileSystem = new(StringComparer.OrdinalIgnoreCase); public void AddFile(string path, Func data) { @@ -18,35 +20,39 @@ public class TestFileSystem : ImageSharp.IO.IFileSystem } } - public Stream Create(string path) + public Stream Create(string path) => this.GetStream(path) ?? File.Create(path); + + public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions { - // if we have injected a fake file use it instead - lock (this.fileSystem) - { - if (this.fileSystem.ContainsKey(path)) - { - Stream stream = this.fileSystem[path](); - stream.Position = 0; - return stream; - } - } + Mode = FileMode.Create, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + }); - return File.Create(path); - } + public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path); + + public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous, + }); - public Stream OpenRead(string path) + private Stream? GetStream(string path) { // if we have injected a fake file use it instead lock (this.fileSystem) { - if (this.fileSystem.ContainsKey(path)) + if (this.fileSystem.TryGetValue(path, out Func? streamFactory)) { - Stream stream = this.fileSystem[path](); + Stream stream = streamFactory(); stream.Position = 0; return stream; } } - return File.OpenRead(path); + return null; } } diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs index 732948b8e0..7b519531ab 100644 --- a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs +++ b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs @@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem Stream IFileSystem.Create(string path) => this.stream; + Stream IFileSystem.CreateAsynchronous(string path) => this.stream; + Stream IFileSystem.OpenRead(string path) => this.stream; + + Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream; } From c81e63984237c451db897f01e6152a48d2c3a543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 15:22:24 +0200 Subject: [PATCH 10/11] Use `await using` where available --- tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 30818f999a..10acd61605 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -91,7 +91,7 @@ public class LocalFileSystemTests LocalFileSystem fs = new(); await using (Stream stream = fs.CreateAsynchronous(path)) - using (StreamWriter writer = new(stream)) + await using (StreamWriter writer = new(stream)) { await writer.WriteAsync(testData); } From 7a2631bdf011e415a2801b2b2e94a9a443da1a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 15:30:20 +0200 Subject: [PATCH 11/11] Add assertions for `FileStream` properties --- .../IO/LocalFileSystemTests.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 10acd61605..a1eeb25976 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -18,9 +18,13 @@ public class LocalFileSystemTests LocalFileSystem fs = new(); - using (Stream stream = fs.OpenRead(path)) + using (FileStream stream = (FileStream)fs.OpenRead(path)) using (StreamReader reader = new(stream)) { + Assert.False(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + string data = reader.ReadToEnd(); Assert.Equal(testData, data); @@ -43,9 +47,13 @@ public class LocalFileSystemTests LocalFileSystem fs = new(); - await using (Stream stream = fs.OpenReadAsynchronous(path)) + await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path)) using (StreamReader reader = new(stream)) { + Assert.True(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + string data = await reader.ReadToEndAsync(); Assert.Equal(testData, data); @@ -66,9 +74,13 @@ public class LocalFileSystemTests string testData = Guid.NewGuid().ToString(); LocalFileSystem fs = new(); - using (Stream stream = fs.Create(path)) + using (FileStream stream = (FileStream)fs.Create(path)) using (StreamWriter writer = new(stream)) { + Assert.False(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.True(stream.CanWrite); + writer.Write(testData); } @@ -90,9 +102,13 @@ public class LocalFileSystemTests string testData = Guid.NewGuid().ToString(); LocalFileSystem fs = new(); - await using (Stream stream = fs.CreateAsynchronous(path)) + await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path)) await using (StreamWriter writer = new(stream)) { + Assert.True(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.True(stream.CanWrite); + await writer.WriteAsync(testData); }