From f8573e334a4673a31665ee280a10b0e4eb96a15a Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Thu, 10 Aug 2017 15:08:14 +0700 Subject: [PATCH 01/99] Add an equivalent for System.Drawing.Image.GetPixelFormatSize() (#258) --- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 11 +++ src/ImageSharp/Formats/Gif/GifDecoder.cs | 11 +++ src/ImageSharp/Formats/IImageDecoder.cs | 8 ++ src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 11 +++ .../Formats/Jpeg/JpegDecoderCore.cs | 22 +++++- src/ImageSharp/Formats/Png/PngDecoder.cs | 12 +++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 74 ++++++++++++++++++ src/ImageSharp/Image/Image.Decode.cs | 14 ++++ src/ImageSharp/Image/Image.FromStream.cs | 27 +++++++ .../Formats/Bmp/BmpDecoderTests.cs | 29 +++++++ .../Formats/Gif/GifDecoderTests.cs | 15 ++++ .../Formats/Jpg/JpegDecoderTests.cs | 16 ++++ .../Formats/Png/PngDecoderTests.cs | 18 +++++ .../Image/ImageDiscoverMimeType.cs | 7 +- .../ImageSharp.Tests/Image/ImageLoadTests.cs | 26 +++--- tests/ImageSharp.Tests/TestFileSystem.cs | 2 +- tests/ImageSharp.Tests/TestFormat.cs | 15 ++-- tests/ImageSharp.Tests/TestImages.cs | 6 +- .../TestImages/Formats/Bmp/bpp8.bmp | Bin 0 -> 65002 bytes .../TestImages/Formats/Png/bpp1.png | Bin 0 -> 4403 bytes .../TestImages/Formats/Png/gray_4bpp.png | Bin 0 -> 7396 bytes .../TestImages/Formats/Png/palette-8bpp.png | Bin 0 -> 9171 bytes 22 files changed, 297 insertions(+), 27 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Bmp/bpp8.bmp create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Png/bpp1.png create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Png/gray_4bpp.png create mode 100644 tests/ImageSharp.Tests/TestImages/Formats/Png/palette-8bpp.png diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 5baf1b1a5a..bdd15c2d71 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -37,5 +37,16 @@ namespace ImageSharp.Formats return new BmpDecoderCore(configuration, this).Decode(stream); } + + /// + public int DetectPixelSize(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, "stream"); + + byte[] buffer = new byte[2]; + stream.Skip(28); + stream.Read(buffer, 0, 2); + return BitConverter.ToInt16(buffer, 0); + } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 927289094f..4d847c9fe7 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -33,5 +33,16 @@ namespace ImageSharp.Formats var decoder = new GifDecoderCore(configuration, this); return decoder.Decode(stream); } + + /// + public int DetectPixelSize(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, "stream"); + + byte[] buffer = new byte[1]; + stream.Skip(10); // Skip the identifier and size + stream.Read(buffer, 0, 1); // Skip the identifier and size + return (buffer[0] & 0x07) + 1; // The lowest 3 bits represent the bit depth minus 1 + } } } diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index 66eabb1b82..a16ef2612c 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -25,5 +25,13 @@ namespace ImageSharp.Formats /// The decoded image Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel; + + /// + /// Detects the image pixel size from the specified stream. + /// + /// The configuration for the image. + /// The containing image data. + /// The color depth, in number of bits per pixel + int DetectPixelSize(Configuration configuration, Stream stream); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index b3caddeca7..8bdbdfe0cf 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -32,5 +32,16 @@ namespace ImageSharp.Formats return decoder.Decode(stream); } } + + /// + public int DetectPixelSize(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, "stream"); + + using (JpegDecoderCore decoder = new JpegDecoderCore(configuration, this)) + { + return decoder.DetectPixelSize(stream); + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 0ce927e516..60c9f1a1d5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -30,6 +30,11 @@ namespace ImageSharp.Formats /// public const int MaxTq = 3; + /// + /// The only supported precision + /// + public const int SupportedPrecision = 8; + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P #pragma warning disable SA1401 // FieldsMustBePrivate @@ -191,7 +196,7 @@ namespace ImageSharp.Formats public bool IgnoreMetadata { get; private set; } /// - /// Decodes the image from the specified and sets + /// Decodes the image from the specified and sets /// the data to image. /// /// The pixel format. @@ -208,6 +213,17 @@ namespace ImageSharp.Formats return image; } + /// + /// Detects the image pixel size from the specified stream. + /// + /// The containing image data. + /// The color depth, in number of bits per pixel + public int DetectPixelSize(Stream stream) + { + this.ProcessStream(new ImageMetaData(), stream, true); + return this.ComponentCount * SupportedPrecision; + } + /// /// Dispose /// @@ -1196,7 +1212,7 @@ namespace ImageSharp.Formats this.InputProcessor.ReadFull(this.Temp, 0, remaining); // We only support 8-bit precision. - if (this.Temp[0] != 8) + if (this.Temp[0] != SupportedPrecision) { throw new ImageFormatException("Only 8-Bit precision supported."); } @@ -1238,7 +1254,7 @@ namespace ImageSharp.Formats if (h == 3 || v == 3) { - throw new ImageFormatException("Lnsupported subsampling ratio"); + throw new ImageFormatException("Unsupported subsampling ratio"); } switch (this.ComponentCount) diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 61a8cb2127..dd71b70cc9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -56,5 +56,17 @@ namespace ImageSharp.Formats var decoder = new PngDecoderCore(configuration, this); return decoder.Decode(stream); } + + /// + /// Detects the image pixel size from the specified stream. + /// + /// The configuration for the image. + /// The containing image data. + /// The color depth, in number of bits per pixel + public int DetectPixelSize(Configuration configuration, Stream stream) + { + var decoder = new PngDecoderCore(configuration, this); + return decoder.DetectPixelSize(stream); + } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 467d41ff41..7baf35295f 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -261,6 +261,58 @@ namespace ImageSharp.Formats } } + /// + /// Detects the image pixel size from the specified stream. + /// + /// The containing image data. + /// The color depth, in number of bits per pixel + public int DetectPixelSize(Stream stream) + { + this.currentStream = stream; + this.currentStream.Skip(8); + try + { + PngChunk currentChunk; + while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null) + { + try + { + switch (currentChunk.Type) + { + case PngChunkTypes.Header: + this.ReadHeaderChunk(currentChunk.Data); + this.ValidateHeader(); + this.isEndChunkReached = true; + break; + case PngChunkTypes.End: + this.isEndChunkReached = true; + break; + } + } + finally + { + // Data is rented in ReadChunkData() + if (currentChunk.Data != null) + { + ArrayPool.Shared.Return(currentChunk.Data); + } + } + } + } + finally + { + this.scanline?.Dispose(); + this.previousScanline?.Dispose(); + } + + if (this.header == null) + { + throw new ImageFormatException("PNG Image hasn't header chunk"); + } + + return this.CalculateBitsPerPixel(); + } + /// /// Converts a byte array to a new array where each value in the original array is represented by the specified number of bits. /// @@ -343,6 +395,28 @@ namespace ImageSharp.Formats this.scanline = Buffer.CreateClean(this.bytesPerScanline); } + /// + /// Calculates the correct number of bits per pixel for the given color type. + /// + /// The + private int CalculateBitsPerPixel() + { + switch (this.pngColorType) + { + case PngColorType.Grayscale: + case PngColorType.Palette: + return this.header.BitDepth; + case PngColorType.GrayscaleWithAlpha: + return this.header.BitDepth * 2; + case PngColorType.Rgb: + return this.header.BitDepth * 3; + case PngColorType.RgbWithAlpha: + return this.header.BitDepth * 4; + default: + throw new NotSupportedException("Unsupported PNG color type"); + } + } + /// /// Calculates the correct number of bytes per pixel for the given color type. /// diff --git a/src/ImageSharp/Image/Image.Decode.cs b/src/ImageSharp/Image/Image.Decode.cs index 1013107062..05a01d825c 100644 --- a/src/ImageSharp/Image/Image.Decode.cs +++ b/src/ImageSharp/Image/Image.Decode.cs @@ -81,5 +81,19 @@ namespace ImageSharp Image img = decoder.Decode(config, stream); return (img, format); } + + /// + /// Detects the image pixel size. + /// + /// The stream. + /// the configuration. + /// + /// The color depth, in number of bits per pixel or null if suitable decoder not found. + /// + private static int? InternalDetectPixelSize(Stream stream, Configuration config) + { + IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat _); + return decoder?.DetectPixelSize(config, stream); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Image/Image.FromStream.cs b/src/ImageSharp/Image/Image.FromStream.cs index 29d93ae859..032c81c33d 100644 --- a/src/ImageSharp/Image/Image.FromStream.cs +++ b/src/ImageSharp/Image/Image.FromStream.cs @@ -39,6 +39,33 @@ namespace ImageSharp return WithSeekableStream(stream, s => InternalDetectFormat(s, config ?? Configuration.Default)); } + /// + /// By reading the header on the provided stream this calculates the images color depth. + /// + /// The image stream to read the header from. + /// + /// Thrown if the stream is not readable nor seekable. + /// + /// The color depth, in number of bits per pixel or null if suitable decoder not found + public static int? DetectPixelSize(Stream stream) + { + return DetectPixelSize(null, stream); + } + + /// + /// By reading the header on the provided stream this calculates the images color depth. + /// + /// The configuration. + /// The image stream to read the header from. + /// + /// Thrown if the stream is not readable nor seekable. + /// + /// The color depth, in number of bits per pixel or null if suitable decoder not found + public static int? DetectPixelSize(Configuration config, Stream stream) + { + return WithSeekableStream(stream, s => InternalDetectPixelSize(s, config ?? Configuration.Default)); + } + /// /// Create a new instance of the class from the given stream. /// diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs new file mode 100644 index 0000000000..a2eaf6df62 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Tests +{ + using System.IO; + + using Xunit; + + public class BmpDecoderTests + { + [Theory] + [InlineData(TestImages.Bmp.Car, 24)] + [InlineData(TestImages.Bmp.F, 24)] + [InlineData(TestImages.Bmp.NegHeight, 24)] + [InlineData(TestImages.Bmp.Bpp8, 8)] + public void DetectPixelSize(string imagePath, int expectedPixelSize) + { + TestFile testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 06bfd8990d..c952cd799c 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -6,6 +6,7 @@ // ReSharper disable InconsistentNaming namespace ImageSharp.Tests { + using System.IO; using System.Text; using Xunit; @@ -80,5 +81,19 @@ namespace ImageSharp.Tests Assert.Equal("浉条卥慨灲", image.MetaData.Properties[0].Value); } } + + [Theory] + [InlineData(TestImages.Gif.Cheers, 8)] + [InlineData(TestImages.Gif.Giphy, 8)] + [InlineData(TestImages.Gif.Rings, 8)] + [InlineData(TestImages.Gif.Trans, 8)] + public void DetectPixelSize(string imagePath, int expectedPixelSize) + { + TestFile testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 9401d098de..4a9eb43252 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -152,5 +152,21 @@ namespace ImageSharp.Tests Assert.Null(image.MetaData.ExifProfile); } } + + [Theory] + [InlineData(TestImages.Jpeg.Progressive.Progress, 24)] + [InlineData(TestImages.Jpeg.Progressive.Fb, 24)] + [InlineData(TestImages.Jpeg.Baseline.Cmyk, 32)] + [InlineData(TestImages.Jpeg.Baseline.Ycck, 32)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg400, 8)] + [InlineData(TestImages.Jpeg.Baseline.Snake, 24)] + public void DetectPixelSize(string imagePath, int expectedPixelSize) + { + TestFile testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index ee8a2ee55b..fcc0010ec7 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -5,6 +5,7 @@ namespace ImageSharp.Tests { + using System.IO; using System.Text; using Xunit; @@ -82,5 +83,22 @@ namespace ImageSharp.Tests Assert.Equal("潓瑦慷敲", image.MetaData.Properties[0].Name); } } + + [Theory] + [InlineData(TestImages.Png.Bpp1, 1)] + [InlineData(TestImages.Png.Gray4Bpp, 4)] + [InlineData(TestImages.Png.Palette8Bpp, 8)] + [InlineData(TestImages.Png.Pd, 24)] + [InlineData(TestImages.Png.Blur, 32)] + [InlineData(TestImages.Png.Rgb48Bpp, 48)] + [InlineData(TestImages.Png.Rgb48BppInterlaced, 48)] + public void DetectPixelSize(string imagePath, int expectedPixelSize) + { + TestFile testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Image/ImageDiscoverMimeType.cs b/tests/ImageSharp.Tests/Image/ImageDiscoverMimeType.cs index 59a39c4542..0d0d00957e 100644 --- a/tests/ImageSharp.Tests/Image/ImageDiscoverMimeType.cs +++ b/tests/ImageSharp.Tests/Image/ImageDiscoverMimeType.cs @@ -41,20 +41,20 @@ namespace ImageSharp.Tests this.fileSystem = new Mock(); - this.LocalConfiguration = new Configuration() + this.LocalConfiguration = new Configuration { FileSystem = this.fileSystem.Object }; this.LocalConfiguration.AddImageFormatDetector(this.localMimeTypeDetector.Object); - TestFormat.RegisterGloablTestFormat(); + TestFormat.RegisterGlobalTestFormat(); this.Marker = Guid.NewGuid().ToByteArray(); this.DataStream = TestFormat.GlobalTestFormat.CreateStream(this.Marker); this.FilePath = Guid.NewGuid().ToString(); this.fileSystem.Setup(x => x.OpenRead(this.FilePath)).Returns(this.DataStream); - TestFileSystem.RegisterGloablTestFormat(); + TestFileSystem.RegisterGlobalTestFormat(); TestFileSystem.Global.AddFile(this.FilePath, this.DataStream); } @@ -86,7 +86,6 @@ namespace ImageSharp.Tests Assert.Equal(localImageFormat, type); } - [Fact] public void DiscoverImageFormatStream() { diff --git a/tests/ImageSharp.Tests/Image/ImageLoadTests.cs b/tests/ImageSharp.Tests/Image/ImageLoadTests.cs index bb64ceda34..bfbdd93f17 100644 --- a/tests/ImageSharp.Tests/Image/ImageLoadTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageLoadTests.cs @@ -55,28 +55,28 @@ namespace ImageSharp.Tests this.fileSystem = new Mock(); - this.LocalConfiguration = new Configuration() + this.LocalConfiguration = new Configuration { FileSystem = this.fileSystem.Object }; this.LocalConfiguration.AddImageFormatDetector(this.localMimeTypeDetector.Object); this.LocalConfiguration.SetDecoder(localImageFormatMock.Object, this.localDecoder.Object); - TestFormat.RegisterGloablTestFormat(); + TestFormat.RegisterGlobalTestFormat(); this.Marker = Guid.NewGuid().ToByteArray(); this.DataStream = TestFormat.GlobalTestFormat.CreateStream(this.Marker); this.FilePath = Guid.NewGuid().ToString(); this.fileSystem.Setup(x => x.OpenRead(this.FilePath)).Returns(this.DataStream); - TestFileSystem.RegisterGloablTestFormat(); + TestFileSystem.RegisterGlobalTestFormat(); TestFileSystem.Global.AddFile(this.FilePath, this.DataStream); } [Fact] public void LoadFromStream() { - Image img = Image.Load(this.DataStream); + Image img = Image.Load(this.DataStream); Assert.NotNull(img); @@ -87,7 +87,7 @@ namespace ImageSharp.Tests public void LoadFromNoneSeekableStream() { NoneSeekableStream stream = new NoneSeekableStream(this.DataStream); - Image img = Image.Load(stream); + Image img = Image.Load(stream); Assert.NotNull(img); @@ -112,7 +112,7 @@ namespace ImageSharp.Tests public void LoadFromStreamWithConfig() { Stream stream = new MemoryStream(); - Image img = Image.Load(this.LocalConfiguration, stream); + Image img = Image.Load(this.LocalConfiguration, stream); Assert.NotNull(img); @@ -138,7 +138,7 @@ namespace ImageSharp.Tests public void LoadFromStreamWithDecoder() { Stream stream = new MemoryStream(); - Image img = Image.Load(stream, this.localDecoder.Object); + Image img = Image.Load(stream, this.localDecoder.Object); Assert.NotNull(img); this.localDecoder.Verify(x => x.Decode(Configuration.Default, stream)); @@ -158,7 +158,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromBytes() { - Image img = Image.Load(this.DataStream.ToArray()); + Image img = Image.Load(this.DataStream.ToArray()); Assert.NotNull(img); @@ -182,7 +182,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromBytesWithConfig() { - Image img = Image.Load(this.LocalConfiguration, this.DataStream.ToArray()); + Image img = Image.Load(this.LocalConfiguration, this.DataStream.ToArray()); Assert.NotNull(img); @@ -207,7 +207,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromBytesWithDecoder() { - Image img = Image.Load(this.DataStream.ToArray(), this.localDecoder.Object); + Image img = Image.Load(this.DataStream.ToArray(), this.localDecoder.Object); Assert.NotNull(img); this.localDecoder.Verify(x => x.Decode(Configuration.Default, It.IsAny())); @@ -228,7 +228,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromFile() { - Image img = Image.Load(this.DataStream); + Image img = Image.Load(this.DataStream); Assert.NotNull(img); @@ -251,7 +251,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromFileWithConfig() { - Image img = Image.Load(this.LocalConfiguration, this.FilePath); + Image img = Image.Load(this.LocalConfiguration, this.FilePath); Assert.NotNull(img); @@ -273,7 +273,7 @@ namespace ImageSharp.Tests [Fact] public void LoadFromFileWithDecoder() { - Image img = Image.Load(this.FilePath, this.localDecoder.Object); + Image img = Image.Load(this.FilePath, this.localDecoder.Object); Assert.NotNull(img); this.localDecoder.Verify(x => x.Decode(Configuration.Default, this.DataStream)); diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs index d43b989f10..8759704415 100644 --- a/tests/ImageSharp.Tests/TestFileSystem.cs +++ b/tests/ImageSharp.Tests/TestFileSystem.cs @@ -22,7 +22,7 @@ namespace ImageSharp.Tests public static TestFileSystem Global { get; } = new TestFileSystem(); - public static void RegisterGloablTestFormat() + public static void RegisterGlobalTestFormat() { Configuration.Default.FileSystem = Global; } diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index 5a3cd102e7..6c2bca3678 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -23,15 +23,15 @@ namespace ImageSharp.Tests { public static TestFormat GlobalTestFormat { get; } = new TestFormat(); - public static void RegisterGloablTestFormat() + public static void RegisterGlobalTestFormat() { Configuration.Default.Configure(GlobalTestFormat); } public TestFormat() { - this.Encoder = new TestEncoder(this); ; - this.Decoder = new TestDecoder(this); ; + this.Encoder = new TestEncoder(this); + this.Decoder = new TestDecoder(this); } public List DecodeCalls { get; } = new List(); @@ -200,10 +200,15 @@ namespace ImageSharp.Tests config = config }); - // TODO record this happend so we an verify it. + // TODO record this happend so we can verify it. return this.testFormat.Sample(); } + public int DetectPixelSize(Configuration configuration, Stream stream) + { + throw new NotImplementedException(); + } + public bool IsSupportedFileFormat(Span header) => testFormat.IsSupportedFileFormat(header); } @@ -222,7 +227,7 @@ namespace ImageSharp.Tests public void Encode(Image image, Stream stream) where TPixel : struct, IPixel { - // TODO record this happend so we an verify it. + // TODO record this happend so we can verify it. } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 3479457cf8..53e6ae1eff 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -25,6 +25,9 @@ namespace ImageSharp.Tests public const string Powerpoint = "Png/pp.png"; public const string SplashInterlaced = "Png/splash-interlaced.png"; public const string Interlaced = "Png/interlaced.png"; + public const string Palette8Bpp = "Png/Palette-8bpp.png"; + public const string Bpp1 = "Png/bpp1.png"; + public const string Gray4Bpp = "Png/gray_4bpp.png"; public const string Rgb48Bpp = "Png/rgb-48bpp.png"; public const string Rgb48BppInterlaced = "Png/rgb-48bpp-interlaced.png"; @@ -108,8 +111,9 @@ namespace ImageSharp.Tests { public const string Car = "Bmp/Car.bmp"; public const string F = "Bmp/F.bmp"; + public const string Bpp8 = "Bmp/bpp8.bmp"; public const string NegHeight = "Bmp/neg_height.bmp"; - public static readonly string[] All = { Car, F, NegHeight }; + public static readonly string[] All = { Car, F, NegHeight, Bpp8 }; } public static class Gif diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Bmp/bpp8.bmp b/tests/ImageSharp.Tests/TestImages/Formats/Bmp/bpp8.bmp new file mode 100644 index 0000000000000000000000000000000000000000..8ea6f4acd7ccd80842f01fc6d1900a93e9a3863f GIT binary patch literal 65002 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 literal 0 HcmV?d00001 diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Png/bpp1.png b/tests/ImageSharp.Tests/TestImages/Formats/Png/bpp1.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac2c1ee93eb66016e4b45ccb8b498b14d45fdeb GIT binary patch literal 4403 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 WZzy_mMCje0OLa84o3(T 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 Date: Fri, 18 Aug 2017 09:45:02 +0700 Subject: [PATCH 02/99] fixes on code review --- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 4 ++-- src/ImageSharp/Formats/Gif/GifDecoder.cs | 5 +++-- src/ImageSharp/Formats/IImageDecoder.cs | 4 ++-- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 4 ++-- src/ImageSharp/Formats/PixelTypeInfo.cs | 22 +++++++++++++++++++ src/ImageSharp/Formats/Png/PngDecoder.cs | 4 ++-- src/ImageSharp/Image/Image.Decode.cs | 6 ++--- src/ImageSharp/Image/Image.FromStream.cs | 16 +++++++++----- .../Formats/Bmp/BmpDecoderTests.cs | 2 +- .../Formats/Gif/GifDecoderTests.cs | 2 +- .../Formats/Jpg/JpegDecoderTests.cs | 2 +- .../Formats/Png/PngDecoderTests.cs | 2 +- tests/ImageSharp.Tests/TestFormat.cs | 2 +- 13 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 src/ImageSharp/Formats/PixelTypeInfo.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index bdd15c2d71..ee6676f5a1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -39,14 +39,14 @@ namespace ImageSharp.Formats } /// - public int DetectPixelSize(Configuration configuration, Stream stream) + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); byte[] buffer = new byte[2]; stream.Skip(28); stream.Read(buffer, 0, 2); - return BitConverter.ToInt16(buffer, 0); + return new PixelTypeInfo(BitConverter.ToInt16(buffer, 0)); } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 4d847c9fe7..63eb2eaf42 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -35,14 +35,15 @@ namespace ImageSharp.Formats } /// - public int DetectPixelSize(Configuration configuration, Stream stream) + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); byte[] buffer = new byte[1]; stream.Skip(10); // Skip the identifier and size stream.Read(buffer, 0, 1); // Skip the identifier and size - return (buffer[0] & 0x07) + 1; // The lowest 3 bits represent the bit depth minus 1 + int bitsPerPixel = (buffer[0] & 0x07) + 1; // The lowest 3 bits represent the bit depth minus 1 + return new PixelTypeInfo(bitsPerPixel); } } } diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index a16ef2612c..6befafb39e 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -31,7 +31,7 @@ namespace ImageSharp.Formats /// /// The configuration for the image. /// The containing image data. - /// The color depth, in number of bits per pixel - int DetectPixelSize(Configuration configuration, Stream stream); + /// The object + PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 8bdbdfe0cf..66e303c01c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -34,13 +34,13 @@ namespace ImageSharp.Formats } /// - public int DetectPixelSize(Configuration configuration, Stream stream) + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); using (JpegDecoderCore decoder = new JpegDecoderCore(configuration, this)) { - return decoder.DetectPixelSize(stream); + return new PixelTypeInfo(decoder.DetectPixelSize(stream)); } } } diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs new file mode 100644 index 0000000000..c331543515 --- /dev/null +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -0,0 +1,22 @@ +namespace ImageSharp.Formats +{ + /// + /// Stores information about pixel + /// + public class PixelTypeInfo + { + /// + /// Initializes a new instance of the class. + /// + /// color depth, in number of bits per pixel + internal PixelTypeInfo(int bitsPerPixel) + { + this.BitsPerPixel = bitsPerPixel; + } + + /// + /// Gets color depth, in number of bits per pixel + /// + public int BitsPerPixel { get; } + } +} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index dd71b70cc9..b7ffe8e72d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -63,10 +63,10 @@ namespace ImageSharp.Formats /// The configuration for the image. /// The containing image data. /// The color depth, in number of bits per pixel - public int DetectPixelSize(Configuration configuration, Stream stream) + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - return decoder.DetectPixelSize(stream); + return new PixelTypeInfo(decoder.DetectPixelSize(stream)); } } } diff --git a/src/ImageSharp/Image/Image.Decode.cs b/src/ImageSharp/Image/Image.Decode.cs index 05a01d825c..313224c5fa 100644 --- a/src/ImageSharp/Image/Image.Decode.cs +++ b/src/ImageSharp/Image/Image.Decode.cs @@ -88,12 +88,12 @@ namespace ImageSharp /// The stream. /// the configuration. /// - /// The color depth, in number of bits per pixel or null if suitable decoder not found. + /// The or null if suitable decoder not found. /// - private static int? InternalDetectPixelSize(Stream stream, Configuration config) + private static PixelTypeInfo InternalDetectPixelType(Stream stream, Configuration config) { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat _); - return decoder?.DetectPixelSize(config, stream); + return decoder?.DetectPixelType(config, stream); } } } \ No newline at end of file diff --git a/src/ImageSharp/Image/Image.FromStream.cs b/src/ImageSharp/Image/Image.FromStream.cs index 032c81c33d..e84d89a912 100644 --- a/src/ImageSharp/Image/Image.FromStream.cs +++ b/src/ImageSharp/Image/Image.FromStream.cs @@ -46,10 +46,12 @@ namespace ImageSharp /// /// Thrown if the stream is not readable nor seekable. /// - /// The color depth, in number of bits per pixel or null if suitable decoder not found - public static int? DetectPixelSize(Stream stream) + /// + /// The or null if suitable decoder not found. + /// + public static PixelTypeInfo DetectPixelType(Stream stream) { - return DetectPixelSize(null, stream); + return DetectPixelType(null, stream); } /// @@ -60,10 +62,12 @@ namespace ImageSharp /// /// Thrown if the stream is not readable nor seekable. /// - /// The color depth, in number of bits per pixel or null if suitable decoder not found - public static int? DetectPixelSize(Configuration config, Stream stream) + /// + /// The or null if suitable decoder not found. + /// + public static PixelTypeInfo DetectPixelType(Configuration config, Stream stream) { - return WithSeekableStream(stream, s => InternalDetectPixelSize(s, config ?? Configuration.Default)); + return WithSeekableStream(stream, s => InternalDetectPixelType(s, config ?? Configuration.Default)); } /// diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index a2eaf6df62..84edca0014 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -22,7 +22,7 @@ namespace ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index c952cd799c..b7af4525ec 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -92,7 +92,7 @@ namespace ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 4a9eb43252..4ac7c5c5b5 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -165,7 +165,7 @@ namespace ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index c7147b2261..f9b34e7c7f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -97,7 +97,7 @@ namespace ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelSize(stream)); + Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index 6c2bca3678..c760b9b98f 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -204,7 +204,7 @@ namespace ImageSharp.Tests return this.testFormat.Sample(); } - public int DetectPixelSize(Configuration configuration, Stream stream) + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) { throw new NotImplementedException(); } From ebe664cc7a698a1518d1df57bd7849d20f9b29ad Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Thu, 24 Aug 2017 16:16:23 +0700 Subject: [PATCH 03/99] update packages --- src/ImageSharp/ImageSharp.csproj | 8 ++++---- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 2 +- tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs | 6 +++--- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 8733131a74..2748275556 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -38,13 +38,13 @@ All - + - - + + - + ..\..\ImageSharp.ruleset diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 72593a0da4..6228afd9ea 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 0537ffda0e..7b9d28f039 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -3,10 +3,10 @@ // Licensed under the Apache License, Version 2.0. // -using ImageSharp.Formats; - namespace ImageSharp.Tests { + using System.IO; + using ImageSharp.PixelFormats; using Xunit; @@ -38,4 +38,4 @@ namespace ImageSharp.Tests } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index b0429d9ede..bd20ba7c1c 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -10,7 +10,7 @@ - + From d6a2d40a80a60e0eb25861d7714f4cfd5b8cc90d Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Thu, 24 Aug 2017 17:36:40 +0700 Subject: [PATCH 04/99] fix test image filename --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c80fde6d07..8960de8868 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -25,7 +25,7 @@ namespace ImageSharp.Tests public const string Powerpoint = "Png/pp.png"; public const string SplashInterlaced = "Png/splash-interlaced.png"; public const string Interlaced = "Png/interlaced.png"; - public const string Palette8Bpp = "Png/Palette-8bpp.png"; + public const string Palette8Bpp = "Png/palette-8bpp.png"; public const string Bpp1 = "Png/bpp1.png"; public const string Gray4Bpp = "Png/gray_4bpp.png"; public const string Rgb48Bpp = "Png/rgb-48bpp.png"; From 93419ab0e4be428d1e1c81f8a65d31f2b29bdb17 Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Tue, 26 Sep 2017 16:58:11 +0700 Subject: [PATCH 05/99] fix bugs after merge --- .../Formats/Jpeg/GolangPort/OrigJpegDecoder.cs | 11 +++++++++++ src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 3 +-- .../Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs | 6 ++++++ src/ImageSharp/Formats/PixelTypeInfo.cs | 2 +- .../ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs | 2 ++ .../ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs | 2 ++ .../SystemDrawingReferenceDecoder.cs | 8 ++++++++ .../TestUtilities/Tests/TestImageProviderTests.cs | 10 ++++++++++ .../Formats => Images/Input}/Bmp/bpp8.bmp | Bin .../Formats => Images/Input}/Png/bpp1.png | Bin .../Formats => Images/Input}/Png/gray_4bpp.png | Bin .../Formats => Images/Input}/Png/palette-8bpp.png | Bin 12 files changed, 41 insertions(+), 3 deletions(-) rename tests/{ImageSharp.Tests/TestImages/Formats => Images/Input}/Bmp/bpp8.bmp (100%) rename tests/{ImageSharp.Tests/TestImages/Formats => Images/Input}/Png/bpp1.png (100%) rename tests/{ImageSharp.Tests/TestImages/Formats => Images/Input}/Png/gray_4bpp.png (100%) rename tests/{ImageSharp.Tests/TestImages/Formats => Images/Input}/Png/palette-8bpp.png (100%) diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs index 13be70e30b..97a424c415 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs @@ -27,5 +27,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort return decoder.Decode(stream); } } + + /// + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, "stream"); + + using (var decoder = new OrigJpegDecoderCore(configuration, this)) + { + return new PixelTypeInfo(decoder.DetectPixelSize(stream)); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index d4ec048d32..6845314c1c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -4,7 +4,6 @@ using System.IO; using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; -using SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -36,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { Guard.NotNull(stream, "stream"); - using (JpegDecoderCore decoder = new JpegDecoderCore(configuration, this)) + using (var decoder = new OrigJpegDecoderCore(configuration, this)) { return new PixelTypeInfo(decoder.DetectPixelSize(stream)); } diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs index 37ce0151f3..1458d54e5f 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs @@ -27,5 +27,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort return decoder.Decode(stream); } } + + /// + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index c331543515..d4b72a00b4 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -1,4 +1,4 @@ -namespace ImageSharp.Formats +namespace SixLabors.ImageSharp.Formats { /// /// Stores information about pixel diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index c6598fab81..b6f894386b 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -8,6 +8,8 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { + using System.IO; + using SixLabors.ImageSharp.Formats.Bmp; public class BmpDecoderTests : FileTestBase diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index f57b0f128d..a13730eb6c 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -11,6 +11,8 @@ using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests { + using System.IO; + public class GifDecoderTests { private const PixelTypes PixelTypes = Tests.PixelTypes.Rgba32 | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32; diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 23ff61eb3d..d36fd2a14d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -45,5 +45,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } } + + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + { + using (var sourceBitmap = new System.Drawing.Bitmap(stream)) + { + return new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat)); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 88d7fa2b86..24becce2ed 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -83,6 +83,11 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + { + throw new NotImplementedException(); + } + // Couldn't make xUnit happy without this hackery: private static ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); @@ -145,6 +150,11 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } + public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + { + throw new NotImplementedException(); + } + private static ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); private string callerName = null; diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Bmp/bpp8.bmp b/tests/Images/Input/Bmp/bpp8.bmp similarity index 100% rename from tests/ImageSharp.Tests/TestImages/Formats/Bmp/bpp8.bmp rename to tests/Images/Input/Bmp/bpp8.bmp diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Png/bpp1.png b/tests/Images/Input/Png/bpp1.png similarity index 100% rename from tests/ImageSharp.Tests/TestImages/Formats/Png/bpp1.png rename to tests/Images/Input/Png/bpp1.png diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Png/gray_4bpp.png b/tests/Images/Input/Png/gray_4bpp.png similarity index 100% rename from tests/ImageSharp.Tests/TestImages/Formats/Png/gray_4bpp.png rename to tests/Images/Input/Png/gray_4bpp.png diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Png/palette-8bpp.png b/tests/Images/Input/Png/palette-8bpp.png similarity index 100% rename from tests/ImageSharp.Tests/TestImages/Formats/Png/palette-8bpp.png rename to tests/Images/Input/Png/palette-8bpp.png From 4a59c6ff1468c738d40ad496909e3e96d587832c Mon Sep 17 00:00:00 2001 From: Nikita Balabaev Date: Tue, 26 Sep 2017 17:05:38 +0700 Subject: [PATCH 06/99] remove duplicates in .csproj files --- src/ImageSharp/ImageSharp.csproj | 1 - tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 509270cccb..45d0a70b81 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -40,7 +40,6 @@ All - diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index b016fb3bae..e8a6e8c596 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -16,10 +16,6 @@ - - - - From 58f105e52320a6002e5bf7b1c161b7735cd118e5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 Nov 2017 00:55:40 +1100 Subject: [PATCH 07/99] Refactor affine classes to inherit from weighted --- .../Processors/Transforms/AffineProcessor.cs | 95 ++++++++ .../Transforms/Matrix3x2Processor.cs | 50 ---- .../Processors/Transforms/RotateProcessor.cs | 218 +++++++++--------- .../Processors/Transforms/SkewProcessor.cs | 79 +++---- .../Processing/Transforms/Rotate.cs | 3 +- src/ImageSharp/Processing/Transforms/Skew.cs | 4 +- 6 files changed, 249 insertions(+), 200 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs new file mode 100644 index 0000000000..4d70fba84f --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides the base methods to perform affine transforms on an image. + /// + /// The pixel format. + internal abstract class AffineProcessor : ResamplingWeightedProcessor + where TPixel : struct, IPixel + { + // TODO: Move to constants somewhere else to prevent generic type duplication. + private static readonly Rectangle DefaultRectangle = new Rectangle(0, 0, 1, 1); + + /// + /// Initializes a new instance of the class. + /// + /// The sampler to perform the resize operation. + protected AffineProcessor(IResampler sampler) + : base(sampler, 1, 1, DefaultRectangle) // Hack to prevent Guard throwing in base, we always set the canvas + { + } + + /// + /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. + /// + public bool Expand { get; set; } = true; + + /// + /// Returns the processing matrix used for transforming the image. + /// + /// The + protected abstract Matrix3x2 CreateProcessingMatrix(); + + /// + /// Creates a new target canvas to contain the results of the matrix transform. + /// + /// The source rectangle. + protected virtual void CreateNewCanvas(Rectangle sourceRectangle) + { + if (this.ResizeRectangle == DefaultRectangle) + { + if (this.Expand) + { + this.ResizeRectangle = Matrix3x2.Invert(this.CreateProcessingMatrix(), out Matrix3x2 sizeMatrix) + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; + } + else + { + this.ResizeRectangle = sourceRectangle; + } + } + + this.Width = this.ResizeRectangle.Width; + this.Height = this.ResizeRectangle.Height; + } + + /// + protected override Image CreateDestination(Image source, Rectangle sourceRectangle) + { + this.CreateNewCanvas(sourceRectangle); + + // We will always be creating the clone even for mutate because we may need to resize the canvas + IEnumerable> frames = + source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); + + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + } + + /// + /// Gets a transform matrix adjusted to center upon the target image bounds. + /// + /// The source image. + /// The transform matrix. + /// + /// The . + /// + protected Matrix3x2 GetCenteredMatrix(ImageFrame source, Matrix3x2 matrix) + { + var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.ResizeRectangle.Width * .5F, -this.ResizeRectangle.Height * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); + return (translationToTargetCenter * matrix) * translateToSourceCenter; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs b/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs deleted file mode 100644 index 4a15254ab2..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/Matrix3x2Processor.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Provides methods to transform an image using a . - /// - /// The pixel format. - internal abstract class Matrix3x2Processor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Gets the rectangle designating the target canvas. - /// - protected Rectangle CanvasRectangle { get; private set; } - - /// - /// Creates a new target canvas to contain the results of the matrix transform. - /// - /// The source rectangle. - /// The processing matrix. - protected void CreateNewCanvas(Rectangle sourceRectangle, Matrix3x2 processMatrix) - { - Matrix3x2 sizeMatrix; - this.CanvasRectangle = Matrix3x2.Invert(processMatrix, out sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; - } - - /// - /// Gets a transform matrix adjusted to center upon the target image bounds. - /// - /// The source image. - /// The transform matrix. - /// - /// The . - /// - protected Matrix3x2 GetCenteredMatrix(ImageFrame source, Matrix3x2 matrix) - { - var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.CanvasRectangle.Width * .5F, -this.CanvasRectangle.Height * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return (translationToTargetCenter * matrix) * translateToSourceCenter; - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 86a0c73603..f47d483596 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -6,7 +6,6 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -17,46 +16,84 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : Matrix3x2Processor + internal class RotateProcessor : AffineProcessor where TPixel : struct, IPixel { + private Matrix3x2 transformMatrix; + /// - /// The transform matrix to apply. + /// Initializes a new instance of the class. /// - private Matrix3x2 processMatrix; + public RotateProcessor() + : base(new BicubicResampler()) + { + } /// - /// Gets or sets the angle of processMatrix in degrees. + /// Initializes a new instance of the class. /// - public float Angle { get; set; } + /// The sampler to perform the resize operation. + public RotateProcessor(IResampler sampler) + : base(sampler) + { + } /// - /// Gets or sets a value indicating whether to expand the canvas to fit the rotated image. + /// Gets or sets the angle of processMatrix in degrees. /// - public bool Expand { get; set; } = true; + public float Angle { get; set; } /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override Matrix3x2 CreateProcessingMatrix() { - if (this.OptimizedApply(source, configuration)) + if (this.transformMatrix == default(Matrix3x2)) + { + this.transformMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0)); + } + + return this.transformMatrix; + } + + /// + protected override void CreateNewCanvas(Rectangle sourceRectangle) + { + if (MathF.Abs(this.Angle) < Constants.Epsilon || + MathF.Abs(this.Angle - 180) < Constants.Epsilon) + { + this.ResizeRectangle = sourceRectangle; + } + + if (MathF.Abs(this.Angle - 90) < Constants.Epsilon || + MathF.Abs(this.Angle - 270) < Constants.Epsilon) + { + // We always expand enumerated rectangle values + this.ResizeRectangle = new Rectangle(0, 0, sourceRectangle.Height, sourceRectangle.Width); + } + + base.CreateNewCanvas(sourceRectangle); + } + + /// + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + if (this.OptimizedApply(source, destination, configuration)) { return; } - int height = this.CanvasRectangle.Height; - int width = this.CanvasRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix); + int height = this.ResizeRectangle.Height; + int width = this.ResizeRectangle.Width; + Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + // TODO: Use our new weights functionality to resample on transform + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => { - Span targetRow = targetPixels.GetRowSpan(y); + Span destRow = destination.GetPixelRowSpan(y); for (int x = 0; x < width; x++) { @@ -64,34 +101,16 @@ namespace SixLabors.ImageSharp.Processing.Processors if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - targetRow[x] = source[transformedPoint.X, transformedPoint.Y]; + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; } } }); - - source.SwapPixelsBuffers(targetPixels); - } - } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - if (MathF.Abs(this.Angle) < Constants.Epsilon || MathF.Abs(this.Angle - 90) < Constants.Epsilon || MathF.Abs(this.Angle - 180) < Constants.Epsilon || MathF.Abs(this.Angle - 270) < Constants.Epsilon) - { - return; - } - - this.processMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0)); - if (this.Expand) - { - this.CreateNewCanvas(sourceRectangle, this.processMatrix); - } } /// - protected override void AfterImageApply(Image source, Rectangle sourceRectangle) + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) { - ExifProfile profile = source.MetaData.ExifProfile; + ExifProfile profile = destination.MetaData.ExifProfile; if (profile == null) { return; @@ -116,33 +135,35 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Rotates the images with an optimized method when the angle is 90, 180 or 270 degrees. /// /// The source image. + /// The destination image. /// The configuration. /// /// The /// - private bool OptimizedApply(ImageFrame source, Configuration configuration) + private bool OptimizedApply(ImageFrame source, ImageFrame destination, Configuration configuration) { if (MathF.Abs(this.Angle) < Constants.Epsilon) { - // No need to do anything so return. + // The destination will be blank here so copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return true; } if (MathF.Abs(this.Angle - 90) < Constants.Epsilon) { - this.Rotate90(source, configuration); + this.Rotate90(source, destination, configuration); return true; } if (MathF.Abs(this.Angle - 180) < Constants.Epsilon) { - this.Rotate180(source, configuration); + this.Rotate180(source, destination, configuration); return true; } if (MathF.Abs(this.Angle - 270) < Constants.Epsilon) { - this.Rotate270(source, configuration); + this.Rotate270(source, destination, configuration); return true; } @@ -153,95 +174,82 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Rotates the image 270 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate270(ImageFrame source, Configuration configuration) + private void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(height, width)) - { - using (PixelAccessor sourcePixels = source.Lock()) + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - for (int x = 0; x < width; x++) - { - int newX = height - y - 1; - newX = height - newX - 1; - int newY = width - x - 1; - targetPixels[newX, newY] = sourcePixels[x, y]; - } - }); - } + Span sourceRow = source.GetPixelRowSpan(y); + for (int x = 0; x < width; x++) + { + int newX = height - y - 1; + newX = height - newX - 1; + int newY = width - x - 1; - source.SwapPixelsBuffers(targetPixels); - } + destination[newX, newY] = sourceRow[x]; + } + }); } /// /// Rotates the image 180 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate180(ImageFrame source, Configuration configuration) + private void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(height - y - 1); - - for (int x = 0; x < width; x++) - { - targetRow[width - x - 1] = sourceRow[x]; - } - }); + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = destination.GetPixelRowSpan(height - y - 1); - source.SwapPixelsBuffers(targetPixels); - } + for (int x = 0; x < width; x++) + { + targetRow[width - x - 1] = sourceRow[x]; + } + }); } /// /// Rotates the image 90 degrees clockwise at the centre point. /// /// The source image. + /// The destination image. /// The configuration. - private void Rotate90(ImageFrame source, Configuration configuration) + private void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) { int width = source.Width; int height = source.Height; - using (var targetPixels = new PixelAccessor(height, width)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span sourceRow = source.GetPixelRowSpan(y); + int newX = height - y - 1; + for (int x = 0; x < width; x++) { - Span sourceRow = source.GetPixelRowSpan(y); - int newX = height - y - 1; - for (int x = 0; x < width; x++) - { - targetPixels[newX, x] = sourceRow[x]; - } - }); - - source.SwapPixelsBuffers(targetPixels); - } + destination[newX, x] = sourceRow[x]; + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 316e2a2af3..ba84eab9e8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -6,7 +6,6 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -16,13 +15,19 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : Matrix3x2Processor + internal class SkewProcessor : AffineProcessor where TPixel : struct, IPixel { + private Matrix3x2 transformMatrix; + /// - /// The transform matrix to apply. + /// Initializes a new instance of the class. /// - private Matrix3x2 processMatrix; + /// The sampler to perform the skew operation. + public SkewProcessor(IResampler sampler) + : base(sampler) + { + } /// /// Gets or sets the angle of rotation along the x-axis in degrees. @@ -34,52 +39,44 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public float AngleY { get; set; } - /// - /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. - /// - public bool Expand { get; set; } = true; + /// + protected override Matrix3x2 CreateProcessingMatrix() + { + if (this.transformMatrix == default(Matrix3x2)) + { + this.transformMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0)); + } + + return this.transformMatrix; + } /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { - int height = this.CanvasRectangle.Height; - int width = this.CanvasRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.processMatrix); + int height = this.ResizeRectangle.Height; + int width = this.ResizeRectangle.Width; + Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => + // TODO: Use our new weights functionality to resample on transform + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) { - Span targetRow = targetPixels.GetRowSpan(y); + var transformedPoint = Point.Skew(new Point(x, y), matrix); - for (int x = 0; x < width; x++) + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - var transformedPoint = Point.Skew(new Point(x, y), matrix); - - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - targetRow[x] = source[transformedPoint.X, transformedPoint.Y]; - } + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; } - }); - - source.SwapPixelsBuffers(targetPixels); - } - } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - this.processMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0)); - if (this.Expand) - { - this.CreateNewCanvas(sourceRectangle, this.processMatrix); - } + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index 7b35a879bc..436aa3cd98 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; @@ -47,6 +46,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new RotateProcessor { Angle = degrees, Expand = expand }); + => source.ApplyProcessor(new RotateProcessor(new BicubicResampler()) { Angle = degrees, Expand = expand }); } } diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index 00411946d9..873dbe5100 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; namespace SixLabors.ImageSharp @@ -37,6 +37,6 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new SkewProcessor { AngleX = degreesX, AngleY = degreesY, Expand = expand }); + => source.ApplyProcessor(new SkewProcessor(new BicubicResampler()) { AngleX = degreesX, AngleY = degreesY, Expand = expand }); } } From ab5dcdf7a2843d112a845d3387c7b5341e9504e3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 Nov 2017 20:15:02 +1100 Subject: [PATCH 08/99] Add broken implementation. Help needed! --- .../Processors/Transforms/AffineProcessor.cs | 44 +++++++++++++++++++ .../Processors/Transforms/RotateProcessor.cs | 36 +++++++++++++-- .../Processors/Transforms/SkewProcessor.cs | 37 ++++++++++++++-- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 4d70fba84f..87f28045c8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -91,5 +92,48 @@ namespace SixLabors.ImageSharp.Processing.Processors var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); return (translationToTargetCenter * matrix) * translateToSourceCenter; } + + /// + /// Computes the weighted sum at the given XY position + /// + /// The source image + /// The maximum x value + /// The maximum y value + /// The horizontal weights + /// The vertical weights + /// The transformed position + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected Vector4 ComputeWeightedSumAtPosition(ImageFrame source, int maxX, int maxY, ref WeightsWindow windowX, ref WeightsWindow windowY, ref Point point) + { + ref float horizontalValues = ref windowX.GetStartReference(); + ref float verticalValues = ref windowY.GetStartReference(); + int xLeft = windowX.Left; + int yLeft = windowY.Left; + int xLength = windowX.Length; + int yLength = windowY.Length; + Vector4 result = Vector4.Zero; + + // TODO: Fix this. + // Currently the output image is the separable values duplicated with half the transform applied + // and not the combined values as it should be. I must be sampling the wrong values. + for (int i = 0; i < xLength; i++) + { + int offsetX = xLeft + i + point.X; + offsetX = offsetX.Clamp(0, maxX); + float weight = Unsafe.Add(ref horizontalValues, i); + result += source[offsetX, point.Y].ToVector4() * weight; + } + + for (int i = 0; i < yLength; i++) + { + int offsetY = yLeft + i + point.Y; + offsetY = offsetY.Clamp(0, maxY); + float weight = Unsafe.Add(ref verticalValues, i); + result += source[point.X, offsetY].ToVector4() * weight; + } + + return result; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index f47d483596..50b28a8312 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -86,7 +86,33 @@ namespace SixLabors.ImageSharp.Processing.Processors Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - // TODO: Use our new weights functionality to resample on transform + if (this.Sampler is NearestNeighborResampler) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var transformedPoint = Point.Rotate(new Point(x, y), matrix); + + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + { + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + } + } + }); + + return; + } + + int maxX = source.Height - 1; + int maxY = source.Width - 1; + Parallel.For( 0, height, @@ -94,14 +120,16 @@ namespace SixLabors.ImageSharp.Processing.Processors y => { Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) { var transformedPoint = Point.Rotate(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; + WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(dXY); } } }); diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index ba84eab9e8..2330559951 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -58,7 +58,33 @@ namespace SixLabors.ImageSharp.Processing.Processors Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); Rectangle sourceBounds = source.Bounds(); - // TODO: Use our new weights functionality to resample on transform + if (this.Sampler is NearestNeighborResampler) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var transformedPoint = Point.Skew(new Point(x, y), matrix); + + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + { + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + } + } + }); + + return; + } + + int maxX = source.Height - 1; + int maxY = source.Width - 1; + Parallel.For( 0, height, @@ -66,14 +92,17 @@ namespace SixLabors.ImageSharp.Processing.Processors y => { Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) { var transformedPoint = Point.Skew(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { - destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; + WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; + + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(dXY); } } }); From a4362f39338c0f12d74672295e28416598ac2fc5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 Nov 2017 22:05:48 +1100 Subject: [PATCH 09/99] Fix max values --- .../Processing/Processors/Transforms/RotateProcessor.cs | 4 ++-- .../Processing/Processors/Transforms/SkewProcessor.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 50b28a8312..1bcd26d8d4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -110,8 +110,8 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - int maxX = source.Height - 1; - int maxY = source.Width - 1; + int maxX = source.Width - 1; + int maxY = source.Height - 1; Parallel.For( 0, diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 2330559951..ba156c6743 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -82,8 +82,8 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - int maxX = source.Height - 1; - int maxY = source.Width - 1; + int maxX = source.Width - 1; + int maxY = source.Height - 1; Parallel.For( 0, From d12d1488893399c05ab25013853922ce930ac285 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 Nov 2017 23:47:09 +1100 Subject: [PATCH 10/99] Better but still not correct --- .../Processors/Transforms/AffineProcessor.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 87f28045c8..93a97267ce 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(this.ResizeRectangle.Width, this.ResizeRectangle.Height, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); @@ -115,22 +115,23 @@ namespace SixLabors.ImageSharp.Processing.Processors Vector4 result = Vector4.Zero; // TODO: Fix this. - // Currently the output image is the separable values duplicated with half the transform applied - // and not the combined values as it should be. I must be sampling the wrong values. - for (int i = 0; i < xLength; i++) + // The output for skew is shrunken, offset, with right/bottom banding. + // For rotate values are offset + for (int y = 0; y < yLength; y++) { - int offsetX = xLeft + i + point.X; - offsetX = offsetX.Clamp(0, maxX); - float weight = Unsafe.Add(ref horizontalValues, i); - result += source[offsetX, point.Y].ToVector4() * weight; - } - - for (int i = 0; i < yLength; i++) - { - int offsetY = yLeft + i + point.Y; + float yweight = Unsafe.Add(ref verticalValues, y); + int offsetY = yLeft + y + point.Y; offsetY = offsetY.Clamp(0, maxY); - float weight = Unsafe.Add(ref verticalValues, i); - result += source[point.X, offsetY].ToVector4() * weight; + + for (int x = 0; x < xLength; x++) + { + float xweight = Unsafe.Add(ref horizontalValues, x); + int offsetX = xLeft + x + point.X; + offsetX = offsetX.Clamp(0, maxX); + float weight = yweight * xweight; + + result += source[offsetX, offsetY].ToVector4() * weight; + } } return result; From cd0d443e72cc7df7938586e8507a91d07b6424fa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2017 12:11:02 +1100 Subject: [PATCH 11/99] Fix affine transforms and cleanup --- .../Processors/Transforms/AffineProcessor.cs | 32 ++++--- .../Processors/Transforms/ResizeProcessor.cs | 14 +-- .../Processors/Transforms/RotateProcessor.cs | 10 +- .../Processors/Transforms/SkewProcessor.cs | 5 +- .../Transforms/Options/ResizeOptions.cs | 2 +- .../Transforms/Resamplers/KnownResamplers.cs | 96 +++++++++++++++++++ .../Processing/Transforms/Resize.cs | 8 +- .../Processing/Transforms/Rotate.cs | 31 +++++- src/ImageSharp/Processing/Transforms/Skew.cs | 33 ++++++- .../Transforms/ResizeProfilingBenchmarks.cs | 2 +- .../Processors/Transforms/ResizeTests.cs | 37 +++---- .../Processors/Transforms/RotateTests.cs | 13 +++ .../Processors/Transforms/SkewTest.cs | 14 +++ 13 files changed, 233 insertions(+), 64 deletions(-) create mode 100644 src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 93a97267ce..9fcc3f4e43 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -18,15 +19,12 @@ namespace SixLabors.ImageSharp.Processing.Processors internal abstract class AffineProcessor : ResamplingWeightedProcessor where TPixel : struct, IPixel { - // TODO: Move to constants somewhere else to prevent generic type duplication. - private static readonly Rectangle DefaultRectangle = new Rectangle(0, 0, 1, 1); - /// /// Initializes a new instance of the class. /// /// The sampler to perform the resize operation. protected AffineProcessor(IResampler sampler) - : base(sampler, 1, 1, DefaultRectangle) // Hack to prevent Guard throwing in base, we always set the canvas + : base(sampler, 1, 1, Rectangles.DefaultRectangle) // Hack to prevent Guard throwing in base, we always set the canvas { } @@ -47,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source rectangle. protected virtual void CreateNewCanvas(Rectangle sourceRectangle) { - if (this.ResizeRectangle == DefaultRectangle) + if (this.ResizeRectangle == Rectangles.DefaultRectangle) { if (this.Expand) { @@ -82,15 +80,14 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Gets a transform matrix adjusted to center upon the target image bounds. /// /// The source image. - /// The transform matrix. /// /// The . /// - protected Matrix3x2 GetCenteredMatrix(ImageFrame source, Matrix3x2 matrix) + protected Matrix3x2 GetCenteredMatrix(ImageFrame source) { var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.ResizeRectangle.Width * .5F, -this.ResizeRectangle.Height * .5F); var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return (translationToTargetCenter * matrix) * translateToSourceCenter; + return (translationToTargetCenter * this.CreateProcessingMatrix()) * translateToSourceCenter; } /// @@ -108,25 +105,20 @@ namespace SixLabors.ImageSharp.Processing.Processors { ref float horizontalValues = ref windowX.GetStartReference(); ref float verticalValues = ref windowY.GetStartReference(); - int xLeft = windowX.Left; - int yLeft = windowY.Left; int xLength = windowX.Length; int yLength = windowY.Length; Vector4 result = Vector4.Zero; - // TODO: Fix this. - // The output for skew is shrunken, offset, with right/bottom banding. - // For rotate values are offset for (int y = 0; y < yLength; y++) { float yweight = Unsafe.Add(ref verticalValues, y); - int offsetY = yLeft + y + point.Y; + int offsetY = y + point.Y; offsetY = offsetY.Clamp(0, maxY); for (int x = 0; x < xLength; x++) { float xweight = Unsafe.Add(ref horizontalValues, x); - int offsetX = xLeft + x + point.X; + int offsetX = x + point.X; offsetX = offsetX.Clamp(0, maxX); float weight = yweight * xweight; @@ -137,4 +129,14 @@ namespace SixLabors.ImageSharp.Processing.Processors return result; } } + + /// + /// Contains a static rectangle used for comparison when creating a new canvas. + /// We do this so we can inherit from the resampling weights class and pass the guard in the constructor and also avoid creating a new rectangle each time. + /// + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "I'm using this only here to prevent duplication in generic types.")] + internal static class Rectangles + { + public static Rectangle DefaultRectangle { get; } = new Rectangle(0, 0, 1, 1); + } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index a4fdb1a1b4..ecfcc7dd20 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -53,16 +53,12 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - Configuration config = source.GetConfiguration(); + // We will always be creating the clone even for mutate because we may need to resize the canvas + IEnumerable> frames = + source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); - // We will always be creating the clone even for mutate because thats the way this base processor works - // ------------ - // For resize we know we are going to populate every pixel with fresh data and we want a different target size so - // let's manually clone an empty set of images at the correct target and then have the base class processs them in turn. - IEnumerable> frames = source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); // this will create places holders - var image = new Image(config, source.MetaData.Clone(), frames); // base the place holder images in to prevet a extra frame being added - - return image; + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 1bcd26d8d4..35ce8ce630 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -25,14 +25,14 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// public RotateProcessor() - : base(new BicubicResampler()) + : base(KnownResamplers.NearestNeighbor) { } /// /// Initializes a new instance of the class. /// - /// The sampler to perform the resize operation. + /// The sampler to perform the rotating operation. public RotateProcessor(IResampler sampler) : base(sampler) { @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.transformMatrix == default(Matrix3x2)) { - this.transformMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, new Point(0, 0)); + this.transformMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, PointF.Empty); } return this.transformMatrix; @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int height = this.ResizeRectangle.Height; int width = this.ResizeRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); + Matrix3x2 matrix = this.GetCenteredMatrix(source); Rectangle sourceBounds = source.Bounds(); if (this.Sampler is NearestNeighborResampler) @@ -99,7 +99,6 @@ namespace SixLabors.ImageSharp.Processing.Processors for (int x = 0; x < width; x++) { var transformedPoint = Point.Rotate(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { destRow[x] = source[transformedPoint.X, transformedPoint.Y]; @@ -127,6 +126,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); ref TPixel dest = ref destRow[x]; dest.PackFromVector4(dXY); diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index ba156c6743..0daf6acdd8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.transformMatrix == default(Matrix3x2)) { - this.transformMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, new Point(0, 0)); + this.transformMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, PointF.Empty); } return this.transformMatrix; @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { int height = this.ResizeRectangle.Height; int width = this.ResizeRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source, this.CreateProcessingMatrix()); + Matrix3x2 matrix = this.GetCenteredMatrix(source); Rectangle sourceBounds = source.Bounds(); if (this.Sampler is NearestNeighborResampler) @@ -71,7 +71,6 @@ namespace SixLabors.ImageSharp.Processing.Processors for (int x = 0; x < width; x++) { var transformedPoint = Point.Skew(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { destRow[x] = source[transformedPoint.X, transformedPoint.Y]; diff --git a/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs b/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs index 8f2d3db0a9..d20eaefb11 100644 --- a/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs +++ b/src/ImageSharp/Processing/Transforms/Options/ResizeOptions.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets or sets the sampler to perform the resize operation. /// - public IResampler Sampler { get; set; } = new BicubicResampler(); + public IResampler Sampler { get; set; } = KnownResamplers.Bicubic; /// /// Gets or sets a value indicating whether to compress diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs b/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs new file mode 100644 index 0000000000..b4a9b648ab --- /dev/null +++ b/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Contains reusable static instances of known resampling algorithms + /// + public static class KnownResamplers + { + /// + /// Gets the Bicubic sampler that implements the bicubic kernel algorithm W(x) + /// + public static IResampler Bicubic { get; } = new BicubicResampler(); + + /// + /// Gets the Box sampler that implements the box algorithm. Similar to nearest neighbor when upscaling. + /// When downscaling the pixels will average, merging pixels together. + /// + public static IResampler Box { get; } = new BoxResampler(); + + /// + /// Gets the Catmull-Rom sampler, a well known standard Cubic Filter often used as a interpolation function + /// + public static IResampler CatmullRom { get; } = new CatmullRomResampler(); + + /// + /// Gets the Hermite sampler. A type of smoothed triangular interpolation filter that rounds off strong edges while + /// preserving flat 'color levels' in the original image. + /// + public static IResampler Hermite { get; } = new HermiteResampler(); + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 2 pixels. + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos2 { get; } = new Lanczos2Resampler(); + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 3 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos3 { get; } = new Lanczos3Resampler(); + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 5 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos5 { get; } = new Lanczos5Resampler(); + + /// + /// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 8 pixels + /// This algorithm provides sharpened results when compared to others when downsampling. + /// + public static IResampler Lanczos8 { get; } = new Lanczos8Resampler(); + + /// + /// Gets the Mitchell-Netravali sampler. This seperable cubic algorithm yields a very good equilibrium between + /// detail preservation (sharpness) and smoothness. + /// + public static IResampler MitchellNetravali { get; } = new MitchellNetravaliResampler(); + + /// + /// Gets the Nearest-Neighbour sampler that implements the nearest neighbor algorithm. This uses a very fast, unscaled filter + /// which will select the closest pixel to the new pixels position. + /// + public static IResampler NearestNeighbor { get; } = new NearestNeighborResampler(); + + /// + /// Gets the Robidoux sampler. This algorithm developed by Nicolas Robidoux providing a very good equilibrium between + /// detail preservation (sharpness) and smoothness comprable to . + /// + public static IResampler Robidoux { get; } = new RobidouxResampler(); + + /// + /// Gets the Robidoux Sharp sampler. A sharpend form of the sampler + /// + public static IResampler RobidouxSharp { get; } = new RobidouxResampler(); + + /// + /// Gets the Spline sampler. A seperable cubic algorithm similar to but yielding smoother results. + /// + public static IResampler Spline { get; } = new SplineResampler(); + + /// + /// Gets the Triangle sampler, otherwise known as Bilinear. This interpolation algorithm can be used where perfect image transformation + /// with pixel matching is impossible, so that one can calculate and assign appropriate intensity values to pixels + /// + public static IResampler Triangle { get; } = new TriangleResampler(); + + /// + /// Gets the Welch sampler. A high speed algorthm that delivers very sharpened results. + /// + public static IResampler Welch { get; } = new WelchResampler(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Resize.cs b/src/ImageSharp/Processing/Transforms/Resize.cs index 3c7cbca311..832b02dea7 100644 --- a/src/ImageSharp/Processing/Transforms/Resize.cs +++ b/src/ImageSharp/Processing/Transforms/Resize.cs @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size) where TPixel : struct, IPixel { - return Resize(source, size.Width, size.Height, new BicubicResampler(), false); + return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, false); } /// @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Resize(this IImageProcessingContext source, Size size, bool compand) where TPixel : struct, IPixel { - return Resize(source, size.Width, size.Height, new BicubicResampler(), compand); + return Resize(source, size.Width, size.Height, KnownResamplers.Bicubic, compand); } /// @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height) where TPixel : struct, IPixel { - return Resize(source, width, height, new BicubicResampler(), false); + return Resize(source, width, height, KnownResamplers.Bicubic, false); } /// @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Resize(this IImageProcessingContext source, int width, int height, bool compand) where TPixel : struct, IPixel { - return Resize(source, width, height, new BicubicResampler(), compand); + return Resize(source, width, height, KnownResamplers.Bicubic, compand); } /// diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index 436aa3cd98..2ec1385bb9 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -21,9 +21,19 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees) where TPixel : struct, IPixel - { - return Rotate(source, degrees, true); - } + => Rotate(source, degrees, true); + + /// + /// Rotates an image by the given angle in degrees, expanding the image to fit the rotated result. + /// + /// The pixel format. + /// The image to rotate. + /// The angle in degrees to perform the rotation. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler) + where TPixel : struct, IPixel + => Rotate(source, degrees, sampler, true); /// /// Rotates and flips an image by the given instructions. @@ -46,6 +56,19 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new RotateProcessor(new BicubicResampler()) { Angle = degrees, Expand = expand }); + => Rotate(source, degrees, KnownResamplers.NearestNeighbor, expand); + + /// + /// Rotates an image by the given angle in degrees using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to rotate. + /// The angle in degrees to perform the rotation. + /// The to perform the resampling. + /// Whether to expand the image to fit the rotated result. + /// The + public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler, bool expand) + where TPixel : struct, IPixel + => source.ApplyProcessor(new RotateProcessor(sampler) { Angle = degrees, Expand = expand }); } } diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index 873dbe5100..f03e60e3da 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -22,9 +22,20 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY) where TPixel : struct, IPixel - { - return Skew(source, degreesX, degreesY, true); - } + => Skew(source, degreesX, degreesY, true); + + /// + /// Skews an image by the given angles in degrees using the given sampler, expanding the image to fit the skewed result. + /// + /// The pixel format. + /// The image to skew. + /// The angle in degrees to perform the rotation along the x-axis. + /// The angle in degrees to perform the rotation along the y-axis. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler) + where TPixel : struct, IPixel + => Skew(source, degreesX, degreesY, sampler, true); /// /// Skews an image by the given angles in degrees. @@ -37,6 +48,20 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, bool expand) where TPixel : struct, IPixel - => source.ApplyProcessor(new SkewProcessor(new BicubicResampler()) { AngleX = degreesX, AngleY = degreesY, Expand = expand }); + => Skew(source, degreesX, degreesY, KnownResamplers.NearestNeighbor, expand); + + /// + /// Skews an image by the given angles in degrees using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to skew. + /// The angle in degrees to perform the rotation along the x-axis. + /// The angle in degrees to perform the rotation along the y-axis. + /// The to perform the resampling. + /// Whether to expand the image to fit the skewed result. + /// The + public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler, bool expand) + where TPixel : struct, IPixel + => source.ApplyProcessor(new SkewProcessor(sampler) { AngleX = degreesX, AngleY = degreesY, Expand = expand }); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs index 963b849d2a..6dd6369802 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // [Fact] public void PrintWeightsData() { - var proc = new ResizeProcessor(new BicubicResampler(), 200, 200); + var proc = new ResizeProcessor(KnownResamplers.Bicubic, 200, 200); ResamplingWeightedProcessor.WeightsBuffer weights = proc.PrecomputeWeights(200, 500); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index a5e21b8ef3..662d03d9c8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -20,19 +20,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public static readonly TheoryData AllReSamplers = new TheoryData { - { "Bicubic", new BicubicResampler() }, - { "Triangle", new TriangleResampler() }, - { "NearestNeighbor", new NearestNeighborResampler() }, - { "Box", new BoxResampler() }, - { "Lanczos3", new Lanczos3Resampler() }, - { "Lanczos5", new Lanczos5Resampler() }, - { "MitchellNetravali", new MitchellNetravaliResampler() }, - { "Lanczos8", new Lanczos8Resampler() }, - { "Hermite", new HermiteResampler() }, - { "Spline", new SplineResampler() }, - { "Robidoux", new RobidouxResampler() }, - { "RobidouxSharp", new RobidouxSharpResampler() }, - { "Welch", new WelchResampler() } + { "Bicubic", KnownResamplers.Bicubic }, + { "Triangle", KnownResamplers.Triangle}, + { "NearestNeighbor", KnownResamplers.NearestNeighbor }, + { "Box", KnownResamplers.Box }, + { "Lanczos2", KnownResamplers.Lanczos2 }, + { "Lanczos3", KnownResamplers.Lanczos3 }, + { "Lanczos5", KnownResamplers.Lanczos5 }, + { "MitchellNetravali", KnownResamplers.MitchellNetravali }, + { "Lanczos8", KnownResamplers.Lanczos8 }, + { "Hermite", KnownResamplers.Hermite }, + { "Spline", KnownResamplers.Spline }, + { "Robidoux", KnownResamplers.Robidoux }, + { "RobidouxSharp", KnownResamplers.RobidouxSharp }, + { "Welch", KnownResamplers.Welch } }; [Theory] @@ -105,7 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var sourceRectangle = new Rectangle(image.Width / 8, image.Height / 8, image.Width / 4, image.Height / 4); var destRectangle = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Resize(image.Width, image.Height, new BicubicResampler(), sourceRectangle, destRectangle, false)); + image.Mutate(x => x.Resize(image.Width, image.Height, KnownResamplers.Bicubic, sourceRectangle, destRectangle, false)); image.DebugSave(provider); image.CompareToReferenceOutput(provider); @@ -286,7 +287,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(2, 0)] public static void BicubicWindowOscillatesCorrectly(float x, float expected) { - var sampler = new BicubicResampler(); + var sampler = KnownResamplers.Bicubic; float result = sampler.GetValue(x); Assert.Equal(result, expected); @@ -300,7 +301,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(2, 0)] public static void TriangleWindowOscillatesCorrectly(float x, float expected) { - var sampler = new TriangleResampler(); + var sampler = KnownResamplers.Triangle; float result = sampler.GetValue(x); Assert.Equal(result, expected); @@ -314,7 +315,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(2, 0)] public static void Lanczos3WindowOscillatesCorrectly(float x, float expected) { - var sampler = new Lanczos3Resampler(); + var sampler = KnownResamplers.Lanczos3; float result = sampler.GetValue(x); Assert.Equal(result, expected); @@ -328,7 +329,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(4, 0)] public static void Lanczos5WindowOscillatesCorrectly(float x, float expected) { - var sampler = new Lanczos5Resampler(); + var sampler = KnownResamplers.Lanczos5; float result = sampler.GetValue(x); Assert.Equal(result, expected); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index 161af43c91..49fe8952be 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs @@ -39,6 +39,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + [Theory] + [WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(RotateFloatValues), 50, 100, DefaultPixelType)] + public void RotateWithSampler(TestImageProvider provider, float value) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.Rotate(value, KnownResamplers.Triangle)); + image.DebugSave(provider, string.Join("_", value, "triangle")); + } + } + [Theory] [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs index 5e83fa620d..6e0d651496 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs @@ -6,6 +6,8 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { + using SixLabors.ImageSharp.Processing; + public class SkewTest : FileTestBase { public static readonly TheoryData SkewValues @@ -26,5 +28,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.DebugSave(provider, string.Join("_", x, y)); } } + + [Theory] + [WithFileCollection(nameof(DefaultFiles), nameof(SkewValues), DefaultPixelType)] + public void ImageShouldSkewWithSampler(TestImageProvider provider, float x, float y) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(i => i.Skew(x, y, KnownResamplers.Triangle)); + image.DebugSave(provider, string.Join("_", x, y, "triangle")); + } + } } } \ No newline at end of file From 75bf4d6d728450220f372e0f38d90f765bc8f6dc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2017 12:25:51 +1100 Subject: [PATCH 12/99] Fix KnownResamplers --- .../Processing/Transforms/Resamplers/KnownResamplers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs b/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs index b4a9b648ab..2da98497b5 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/KnownResamplers.cs @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets the Robidoux Sharp sampler. A sharpend form of the sampler /// - public static IResampler RobidouxSharp { get; } = new RobidouxResampler(); + public static IResampler RobidouxSharp { get; } = new RobidouxSharpResampler(); /// /// Gets the Spline sampler. A seperable cubic algorithm similar to but yielding smoother results. From 92d8b83f9af290ac161244c328b2502bd890496a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2017 12:47:25 +1100 Subject: [PATCH 13/99] Disable Lanczos2 for now, we need test image --- .../Processing/Processors/Transforms/ResizeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 662d03d9c8..92f1af58ed 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { "Triangle", KnownResamplers.Triangle}, { "NearestNeighbor", KnownResamplers.NearestNeighbor }, { "Box", KnownResamplers.Box }, - { "Lanczos2", KnownResamplers.Lanczos2 }, + // { "Lanczos2", KnownResamplers.Lanczos2 }, TODO: Add expected file { "Lanczos3", KnownResamplers.Lanczos3 }, { "Lanczos5", KnownResamplers.Lanczos5 }, { "MitchellNetravali", KnownResamplers.MitchellNetravali }, From 9cc4d49e9f311dfbaf0d4b5793730705f2e22625 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2017 13:08:02 +1100 Subject: [PATCH 14/99] Bump Travis dotnet build https://github.com/travis-ci/travis-ci/issues/8793 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a4f68b1d1b..70501a484b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ matrix: - os: linux # Ubuntu 14.04 dist: trusty sudo: required - dotnet: 1.0.1 + dotnet: 1.0.4 mono: latest # - os: osx # OSX 10.11 # osx_image: xcode7.3.1 From aa150e6e57682d0d8792079e4a691358e8683a22 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 24 Nov 2017 01:42:56 +0100 Subject: [PATCH 15/99] testing with all resamplers --- .../Processors/Transforms/RotateTests.cs | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index 49fe8952be..c30770ada9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs @@ -8,6 +8,9 @@ using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { + using System; + using System.Reflection; + public class RotateTests : FileTestBase { public static readonly TheoryData RotateFloatValues @@ -26,6 +29,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms RotateType.Rotate270 }; + public static readonly TheoryData ResmplerNames = new TheoryData + { + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), + }; + [Theory] [WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateFloatValues), 50, 100, DefaultPixelType)] @@ -40,15 +62,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } [Theory] - [WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(RotateFloatValues), 50, 100, DefaultPixelType)] - public void RotateWithSampler(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(ResmplerNames), 100, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(ResmplerNames), 50, 100, DefaultPixelType)] + public void RotateWithSampler(TestImageProvider provider, string resamplerName) where TPixel : struct, IPixel { + IResampler resampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) { - image.Mutate(x => x.Rotate(value, KnownResamplers.Triangle)); - image.DebugSave(provider, string.Join("_", value, "triangle")); + image.Mutate(x => x.Rotate(50, resampler)); + image.DebugSave(provider, resamplerName); } } @@ -64,5 +88,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.DebugSave(provider, value); } } + + private static IResampler GetResampler(string name) + { + PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); + + if (property == null) + { + throw new Exception("Invalid property name!"); + } + + return (IResampler) property.GetValue(null); + } } } \ No newline at end of file From e752ce06a5996ee20e8ef0969ce0935362f80b61 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Nov 2017 15:59:14 +1100 Subject: [PATCH 16/99] Fix sampling offset --- .../Processors/Transforms/AffineProcessor.cs | 51 ++++++++++++++----- .../Processors/Transforms/RotateProcessor.cs | 3 +- .../Processors/Transforms/SkewProcessor.cs | 3 +- .../Processors/Transforms/RotateTests.cs | 43 ++++++++-------- .../Processors/Transforms/SkewTest.cs | 48 +++++++++++++++-- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 9fcc3f4e43..4db2aacbac 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. + /// Gets or sets a value indicating whether to expand the canvas to fit the transformed image. /// public bool Expand { get; set; } = true; @@ -96,33 +96,58 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source image /// The maximum x value /// The maximum y value + /// The radius of the current sampling window /// The horizontal weights /// The vertical weights /// The transformed position /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected Vector4 ComputeWeightedSumAtPosition(ImageFrame source, int maxX, int maxY, ref WeightsWindow windowX, ref WeightsWindow windowY, ref Point point) + protected Vector4 ComputeWeightedSumAtPosition(ImageFrame source, int maxX, int maxY, int radius, ref WeightsWindow windowX, ref WeightsWindow windowY, ref Point point) { ref float horizontalValues = ref windowX.GetStartReference(); ref float verticalValues = ref windowY.GetStartReference(); - int xLength = windowX.Length; - int yLength = windowY.Length; + + int left = point.X - radius; + int right = point.X + radius; + int top = point.Y - radius; + int bottom = point.Y + radius; + + // Faster than clamping + we know we are only looking in one direction + if (left < 0) + { + left = 0; + } + + if (top < 0) + { + top = 0; + } + + if (right > maxX) + { + right = maxX; + } + + if (bottom > maxY) + { + bottom = maxY; + } + Vector4 result = Vector4.Zero; - for (int y = 0; y < yLength; y++) + // We calculate our sample by iterating up-down/left-right from our transformed point. + // Ignoring the weight of outlying pixels is better for shape preservation on transforms such as skew with samplers that use larger radii. + // We don't offset our window index so that the weight compensates for the missing values + for (int y = top, yl = 0; y <= bottom; y++, yl++) { - float yweight = Unsafe.Add(ref verticalValues, y); - int offsetY = y + point.Y; - offsetY = offsetY.Clamp(0, maxY); + float yweight = Unsafe.Add(ref verticalValues, yl); - for (int x = 0; x < xLength; x++) + for (int x = left, xl = 0; x <= right; x++, xl++) { - float xweight = Unsafe.Add(ref horizontalValues, x); - int offsetX = x + point.X; - offsetX = offsetX.Clamp(0, maxX); + float xweight = Unsafe.Add(ref horizontalValues, xl); float weight = yweight * xweight; - result += source[offsetX, offsetY].ToVector4() * weight; + result += source[x, y].ToVector4() * weight; } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 35ce8ce630..a1b181e69f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -111,6 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int maxX = source.Width - 1; int maxY = source.Height - 1; + int radius = Math.Max((int)this.Sampler.Radius, 1); Parallel.For( 0, @@ -127,7 +128,7 @@ namespace SixLabors.ImageSharp.Processing.Processors WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); ref TPixel dest = ref destRow[x]; dest.PackFromVector4(dXY); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 0daf6acdd8..0f67f724ca 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -83,6 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int maxX = source.Width - 1; int maxY = source.Height - 1; + int radius = Math.Max((int)this.Sampler.Radius, 1); Parallel.For( 0, @@ -99,7 +100,7 @@ namespace SixLabors.ImageSharp.Processing.Processors WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); ref TPixel dest = ref destRow[x]; dest.PackFromVector4(dXY); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index c30770ada9..ca29009bc7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs @@ -29,24 +29,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms RotateType.Rotate270 }; - public static readonly TheoryData ResmplerNames = new TheoryData - { - nameof(KnownResamplers.Bicubic), - nameof(KnownResamplers.Box), - nameof(KnownResamplers.CatmullRom), - nameof(KnownResamplers.Hermite), - nameof(KnownResamplers.Lanczos2), - nameof(KnownResamplers.Lanczos3), - nameof(KnownResamplers.Lanczos5), - nameof(KnownResamplers.Lanczos8), - nameof(KnownResamplers.MitchellNetravali), - nameof(KnownResamplers.NearestNeighbor), - nameof(KnownResamplers.Robidoux), - nameof(KnownResamplers.RobidouxSharp), - nameof(KnownResamplers.Spline), - nameof(KnownResamplers.Triangle), - nameof(KnownResamplers.Welch), - }; + public static readonly TheoryData ResamplerNames + = new TheoryData + { + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), + }; [Theory] [WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)] @@ -62,8 +63,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } [Theory] - [WithTestPatternImages(nameof(ResmplerNames), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(ResmplerNames), 50, 100, DefaultPixelType)] + [WithTestPatternImages(nameof(ResamplerNames), 100, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(ResamplerNames), 50, 100, DefaultPixelType)] public void RotateWithSampler(TestImageProvider provider, string resamplerName) where TPixel : struct, IPixel { @@ -98,7 +99,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms throw new Exception("Invalid property name!"); } - return (IResampler) property.GetValue(null); + return (IResampler)property.GetValue(null); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs index 6e0d651496..8e57327be0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs @@ -6,17 +6,41 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { + using System; + using System.Collections.Generic; + using System.Reflection; + using SixLabors.ImageSharp.Processing; public class SkewTest : FileTestBase { public static readonly TheoryData SkewValues - = new TheoryData + = new TheoryData { { 20, 10 }, { -20, -10 } }; + public static readonly List ResamplerNames + = new List + { + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), + }; + [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(SkewValues), DefaultPixelType)] public void ImageShouldSkew(TestImageProvider provider, float x, float y) @@ -34,11 +58,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public void ImageShouldSkewWithSampler(TestImageProvider provider, float x, float y) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) + foreach (string resamplerName in ResamplerNames) + { + IResampler sampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) + { + image.Mutate(i => i.Skew(x, y, sampler)); + image.DebugSave(provider, string.Join("_", x, y, resamplerName)); + } + } + } + + private static IResampler GetResampler(string name) + { + PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); + + if (property == null) { - image.Mutate(i => i.Skew(x, y, KnownResamplers.Triangle)); - image.DebugSave(provider, string.Join("_", x, y, "triangle")); + throw new Exception("Invalid property name!"); } + + return (IResampler)property.GetValue(null); } } } \ No newline at end of file From 2673ae4cd6cc66eb2f9f7041c154f224d1bc037b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Nov 2017 02:04:28 +1100 Subject: [PATCH 17/99] Update dependencies, remove code duplication --- .../ImageSharp.Drawing.csproj | 6 +- src/ImageSharp/ImageSharp.csproj | 2 +- .../Processors/Transforms/AffineProcessor.cs | 61 ++++++++++++++++++ .../Processors/Transforms/RotateProcessor.cs | 54 +--------------- .../Processors/Transforms/SkewProcessor.cs | 62 ------------------- 5 files changed, 66 insertions(+), 119 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index eb3c29dd98..3e320dccc7 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -36,9 +36,9 @@ - - - + + + All diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 8c22237cf7..f1bcb2ecda 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -34,7 +34,7 @@ - + All diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 4db2aacbac..cffe6674d6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -76,6 +79,64 @@ namespace SixLabors.ImageSharp.Processing.Processors return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); } + /// + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + int height = this.ResizeRectangle.Height; + int width = this.ResizeRectangle.Width; + Matrix3x2 matrix = this.GetCenteredMatrix(source); + Rectangle sourceBounds = source.Bounds(); + + if (this.Sampler is NearestNeighborResampler) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var transformedPoint = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + { + destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + } + } + }); + + return; + } + + int maxX = source.Width - 1; + int maxY = source.Height - 1; + int radius = Math.Max((int)this.Sampler.Radius, 1); + + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + for (int x = 0; x < width; x++) + { + var transformedPoint = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + { + WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; + WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; + + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(dXY); + } + } + }); + } + /// /// Gets a transform matrix adjusted to center upon the target image bounds. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index a1b181e69f..eb6110fa14 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -81,59 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - int height = this.ResizeRectangle.Height; - int width = this.ResizeRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source); - Rectangle sourceBounds = source.Bounds(); - - if (this.Sampler is NearestNeighborResampler) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - - for (int x = 0; x < width; x++) - { - var transformedPoint = Point.Rotate(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - destRow[x] = source[transformedPoint.X, transformedPoint.Y]; - } - } - }); - - return; - } - - int maxX = source.Width - 1; - int maxY = source.Height - 1; - int radius = Math.Max((int)this.Sampler.Radius, 1); - - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) - { - var transformedPoint = Point.Rotate(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; - WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; - - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(dXY); - } - } - }); + base.OnApply(source, destination, sourceRectangle, configuration); } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 0f67f724ca..fc70330e81 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -1,11 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -49,63 +45,5 @@ namespace SixLabors.ImageSharp.Processing.Processors return this.transformMatrix; } - - /// - protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) - { - int height = this.ResizeRectangle.Height; - int width = this.ResizeRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source); - Rectangle sourceBounds = source.Bounds(); - - if (this.Sampler is NearestNeighborResampler) - { - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - - for (int x = 0; x < width; x++) - { - var transformedPoint = Point.Skew(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - destRow[x] = source[transformedPoint.X, transformedPoint.Y]; - } - } - }); - - return; - } - - int maxX = source.Width - 1; - int maxY = source.Height - 1; - int radius = Math.Max((int)this.Sampler.Radius, 1); - - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) - { - var transformedPoint = Point.Skew(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) - { - WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; - WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; - - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(dXY); - } - } - }); - } } } \ No newline at end of file From e51de68840f7788b267a0384251b753a963c0092 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Nov 2017 02:22:28 +1100 Subject: [PATCH 18/99] Add missing myget source --- NuGet.config | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGet.config b/NuGet.config index b2c967cc97..5c234e5ba6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,6 +1,7 @@  + From 345a06e086fcde249a13ca9c35a062dd6f1ac1c7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 26 Nov 2017 02:24:19 +1100 Subject: [PATCH 19/99] Fix nuget.config, Thanks VSCode!! --- NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index 5c234e5ba6..322105d4d4 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,7 @@  - + From 9b003174911ccb878febe0d492fe69f9ae3df567 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 27 Nov 2017 19:37:06 +1100 Subject: [PATCH 20/99] Original maths was totally incorrect This, however, is no better if not, worse. --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 2 +- .../Processors/Transforms/AffineProcessor.cs | 93 ++++++++++++------- .../Processors/Transforms/RotateProcessor.cs | 33 +++---- .../Processors/Transforms/SkewProcessor.cs | 2 +- 4 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 7d781e77fe..fa60f855ee 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp Vector2[] allCorners = { leftTop, rightTop, leftBottom, rightBottom }; float extentX = allCorners.Select(v => v.X).Max() - allCorners.Select(v => v.X).Min(); float extentY = allCorners.Select(v => v.Y).Max() - allCorners.Select(v => v.Y).Min(); - return new Rectangle(0, 0, (int)extentX, (int)extentY); + return new Rectangle(0, 0, (int)MathF.Ceiling(extentX), (int)MathF.Ceiling(extentY)); } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index cffe6674d6..cc48cd91a3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -39,8 +39,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// Returns the processing matrix used for transforming the image. /// + /// The rectangle bounds /// The - protected abstract Matrix3x2 CreateProcessingMatrix(); + protected abstract Matrix3x2 CreateProcessingMatrix(Rectangle rectangle); /// /// Creates a new target canvas to contain the results of the matrix transform. @@ -48,19 +49,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source rectangle. protected virtual void CreateNewCanvas(Rectangle sourceRectangle) { - if (this.ResizeRectangle == Rectangles.DefaultRectangle) - { - if (this.Expand) - { - this.ResizeRectangle = Matrix3x2.Invert(this.CreateProcessingMatrix(), out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; - } - else - { - this.ResizeRectangle = sourceRectangle; - } - } + this.ResizeRectangle = Matrix3x2.Invert(this.CreateProcessingMatrix(sourceRectangle), out Matrix3x2 sizeMatrix) + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; this.Width = this.ResizeRectangle.Width; this.Height = this.ResizeRectangle.Height; @@ -84,8 +75,8 @@ namespace SixLabors.ImageSharp.Processing.Processors { int height = this.ResizeRectangle.Height; int width = this.ResizeRectangle.Width; - Matrix3x2 matrix = this.GetCenteredMatrix(source); Rectangle sourceBounds = source.Bounds(); + Matrix3x2 matrix = this.GetCenteredMatrix(source); if (this.Sampler is NearestNeighborResampler) { @@ -112,24 +103,24 @@ namespace SixLabors.ImageSharp.Processing.Processors int maxX = source.Width - 1; int maxY = source.Height - 1; - int radius = Math.Max((int)this.Sampler.Radius, 1); Parallel.For( 0, height, - configuration.ParallelOptions, + new ParallelOptions { MaxDegreeOfParallelism = 1 }, y => { Span destRow = destination.GetPixelRowSpan(y); for (int x = 0; x < width; x++) { var transformedPoint = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) { WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, radius, ref windowX, ref windowY, ref transformedPoint); + Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); ref TPixel dest = ref destRow[x]; dest.PackFromVector4(dXY); } @@ -148,7 +139,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.ResizeRectangle.Width * .5F, -this.ResizeRectangle.Height * .5F); var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return (translationToTargetCenter * this.CreateProcessingMatrix()) * translateToSourceCenter; + return (translationToTargetCenter * this.CreateProcessingMatrix(this.ResizeRectangle)) * translateToSourceCenter; } /// @@ -157,55 +148,87 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source image /// The maximum x value /// The maximum y value - /// The radius of the current sampling window /// The horizontal weights /// The vertical weights /// The transformed position /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected Vector4 ComputeWeightedSumAtPosition(ImageFrame source, int maxX, int maxY, int radius, ref WeightsWindow windowX, ref WeightsWindow windowY, ref Point point) + protected Vector4 ComputeWeightedSumAtPosition( + ImageFrame source, + int maxX, + int maxY, + ref WeightsWindow windowX, + ref WeightsWindow windowY, + ref Point point) { + // What, in theory, is supposed to happen here is the following... + // + // We identify the maximum possible pixel offsets allowable by the current sampler + // clamping values to ensure that we do not go outwith the bounds of our image. + // + // Then we get the weights of that offset value from our pre-calculated vaues. + // First we grab the weight on the y-axis, then the x-axis and then we multiply + // them together to get the final weight. + // + // Unfortunately this simply does not seem to work! + // The output is rubbish and I cannot see why :( ref float horizontalValues = ref windowX.GetStartReference(); ref float verticalValues = ref windowY.GetStartReference(); - int left = point.X - radius; - int right = point.X + radius; - int top = point.Y - radius; - int bottom = point.Y + radius; + int yLength = windowY.Length; + int xLength = windowX.Length; + int yRadius = (int)MathF.Ceiling((yLength - 1) * .5F); + int xRadius = (int)MathF.Ceiling((xLength - 1) * .5F); + + int left = point.X - xRadius; + int right = point.X + xRadius; + int top = point.Y - yRadius; + int bottom = point.Y + yRadius; + + int yIndex = 0; + int xIndex = 0; // Faster than clamping + we know we are only looking in one direction if (left < 0) { + // Trim the length of our weights iterator across the x-axis. + // Offset our start index across the x-axis. + xIndex = ImageMaths.FastAbs(left); + xLength -= xIndex; left = 0; } if (top < 0) { + // Trim the length of our weights iterator across the y-axis. + // Offset our start index across the y-axis. + yIndex = ImageMaths.FastAbs(top); + yLength -= yIndex; top = 0; } - if (right > maxX) + if (right >= maxX) { - right = maxX; + // Trim the length of our weights iterator across the x-axis. + xLength -= right - maxX; } - if (bottom > maxY) + if (bottom >= maxY) { - bottom = maxY; + // Trim the length of our weights iterator across the y-axis. + yLength -= bottom - maxY; } Vector4 result = Vector4.Zero; // We calculate our sample by iterating up-down/left-right from our transformed point. - // Ignoring the weight of outlying pixels is better for shape preservation on transforms such as skew with samplers that use larger radii. - // We don't offset our window index so that the weight compensates for the missing values - for (int y = top, yl = 0; y <= bottom; y++, yl++) + for (int y = top, yi = yIndex; yi < yLength; y++, yi++) { - float yweight = Unsafe.Add(ref verticalValues, yl); + float yweight = Unsafe.Add(ref verticalValues, yi); - for (int x = left, xl = 0; x <= right; x++, xl++) + for (int x = left, xi = xIndex; xi < xLength; x++, xi++) { - float xweight = Unsafe.Add(ref horizontalValues, xl); + float xweight = Unsafe.Add(ref horizontalValues, xi); float weight = yweight * xweight; result += source[x, y].ToVector4() * weight; diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index eb6110fa14..1ad7263929 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public float Angle { get; set; } /// - protected override Matrix3x2 CreateProcessingMatrix() + protected override Matrix3x2 CreateProcessingMatrix(Rectangle rectangle) { if (this.transformMatrix == default(Matrix3x2)) { @@ -54,25 +54,6 @@ namespace SixLabors.ImageSharp.Processing.Processors return this.transformMatrix; } - /// - protected override void CreateNewCanvas(Rectangle sourceRectangle) - { - if (MathF.Abs(this.Angle) < Constants.Epsilon || - MathF.Abs(this.Angle - 180) < Constants.Epsilon) - { - this.ResizeRectangle = sourceRectangle; - } - - if (MathF.Abs(this.Angle - 90) < Constants.Epsilon || - MathF.Abs(this.Angle - 270) < Constants.Epsilon) - { - // We always expand enumerated rectangle values - this.ResizeRectangle = new Rectangle(0, 0, sourceRectangle.Height, sourceRectangle.Width); - } - - base.CreateNewCanvas(sourceRectangle); - } - /// protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { @@ -157,6 +138,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { int width = source.Width; int height = source.Height; + Rectangle destinationBounds = destination.Bounds(); Parallel.For( 0, @@ -171,7 +153,10 @@ namespace SixLabors.ImageSharp.Processing.Processors newX = height - newX - 1; int newY = width - x - 1; - destination[newX, newY] = sourceRow[x]; + if (destinationBounds.Contains(newX, newY)) + { + destination[newX, newY] = sourceRow[x]; + } } }); } @@ -213,6 +198,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { int width = source.Width; int height = source.Height; + Rectangle destinationBounds = destination.Bounds(); Parallel.For( 0, @@ -224,7 +210,10 @@ namespace SixLabors.ImageSharp.Processing.Processors int newX = height - y - 1; for (int x = 0; x < width; x++) { - destination[newX, x] = sourceRow[x]; + if (destinationBounds.Contains(newX, x)) + { + destination[newX, x] = sourceRow[x]; + } } }); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index fc70330e81..61526747ea 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public float AngleY { get; set; } /// - protected override Matrix3x2 CreateProcessingMatrix() + protected override Matrix3x2 CreateProcessingMatrix(Rectangle rectangle) { if (this.transformMatrix == default(Matrix3x2)) { From 7d4e29591c8e2b0b8a41efe490a8c7ee263001da Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 1 Dec 2017 18:27:59 +1100 Subject: [PATCH 21/99] It works!!!! Now we need to refactor and optimize this. --- .../Processors/Transforms/AffineProcessor.cs | 154 ++++++------------ .../Transforms/Resamplers/BicubicResampler.cs | 3 + .../Transforms/Resamplers/BoxResampler.cs | 3 + .../Resamplers/CatmullRomResampler.cs | 3 + .../Transforms/Resamplers/HermiteResampler.cs | 3 + .../Resamplers/Lanczos2Resampler.cs | 3 + .../Resamplers/Lanczos3Resampler.cs | 3 + .../Resamplers/Lanczos5Resampler.cs | 3 + .../Resamplers/Lanczos8Resampler.cs | 3 + .../Resamplers/MitchellNetravaliResampler.cs | 3 + .../Resamplers/NearestNeighborResampler.cs | 3 + .../Resamplers/RobidouxSharpResampler.cs | 3 + .../Transforms/Resamplers/SplineResampler.cs | 3 + .../Resamplers/TriangleResampler.cs | 3 + .../Transforms/Resamplers/WelchResampler.cs | 5 +- 15 files changed, 96 insertions(+), 102 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index cc48cd91a3..cea7f5953a 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -90,10 +90,10 @@ namespace SixLabors.ImageSharp.Processing.Processors for (int x = 0; x < width; x++) { - var transformedPoint = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + var point = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(point.X, point.Y)) { - destRow[x] = source[transformedPoint.X, transformedPoint.Y]; + destRow[x] = source[point.X, point.Y]; } } }); @@ -101,29 +101,58 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - int maxX = source.Width - 1; - int maxY = source.Height - 1; + int maxSourceX = source.Width - 1; + int maxSourceY = source.Height - 1; + (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + float xScale = xRadiusScale.scale; + float yScale = yRadiusScale.scale; + float xRadius = xRadiusScale.radius; + float yRadius = yRadiusScale.radius; Parallel.For( 0, height, - new ParallelOptions { MaxDegreeOfParallelism = 1 }, + configuration.ParallelOptions, y => { Span destRow = destination.GetPixelRowSpan(y); for (int x = 0; x < width; x++) { - var transformedPoint = Point.Transform(new Point(x, y), matrix); - - if (sourceBounds.Contains(transformedPoint.X, transformedPoint.Y)) + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var point = PointF.Transform(new PointF(x, y), matrix); + int maxX = (int)MathF.Ceiling(point.X + xRadius); + int maxY = (int)MathF.Ceiling(point.Y + yRadius); + int minX = (int)MathF.Floor(point.X - xRadius); + int minY = (int)MathF.Floor(point.Y - yRadius); + + // Clamp sampling pixels to the source image edge + maxX = maxX.Clamp(0, maxSourceX); + minX = minX.Clamp(0, maxSourceX); + maxY = maxY.Clamp(0, maxSourceY); + minY = minY.Clamp(0, maxSourceY); + + if (minX == maxX || minY == maxY) { - WeightsWindow windowX = this.HorizontalWeights.Weights[transformedPoint.X]; - WeightsWindow windowY = this.VerticalWeights.Weights[transformedPoint.Y]; + continue; + } - Vector4 dXY = this.ComputeWeightedSumAtPosition(source, maxX, maxY, ref windowX, ref windowY, ref transformedPoint); - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(dXY); + // It appears these have to be calculated manually. + // Using the precalculated weights give the wrong values. + // TODO: Find a way to speed this up. + Vector4 sum = Vector4.Zero; + for (int i = minX; i <= maxX; i++) + { + float weightX = this.Sampler.GetValue((i - point.X) / xScale); + for (int j = minY; j <= maxY; j++) + { + float weightY = this.Sampler.GetValue((j - point.Y) / yScale); + sum += source[i, j].ToVector4() * weightX * weightY; + } } + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(sum); } }); } @@ -143,99 +172,22 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - /// Computes the weighted sum at the given XY position + /// Calculates the sampling radius for the current sampler /// - /// The source image - /// The maximum x value - /// The maximum y value - /// The horizontal weights - /// The vertical weights - /// The transformed position - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected Vector4 ComputeWeightedSumAtPosition( - ImageFrame source, - int maxX, - int maxY, - ref WeightsWindow windowX, - ref WeightsWindow windowY, - ref Point point) + /// The source dimension size + /// The destination dimension size + /// The radius, and scaling factor + private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) { - // What, in theory, is supposed to happen here is the following... - // - // We identify the maximum possible pixel offsets allowable by the current sampler - // clamping values to ensure that we do not go outwith the bounds of our image. - // - // Then we get the weights of that offset value from our pre-calculated vaues. - // First we grab the weight on the y-axis, then the x-axis and then we multiply - // them together to get the final weight. - // - // Unfortunately this simply does not seem to work! - // The output is rubbish and I cannot see why :( - ref float horizontalValues = ref windowX.GetStartReference(); - ref float verticalValues = ref windowY.GetStartReference(); - - int yLength = windowY.Length; - int xLength = windowX.Length; - int yRadius = (int)MathF.Ceiling((yLength - 1) * .5F); - int xRadius = (int)MathF.Ceiling((xLength - 1) * .5F); - - int left = point.X - xRadius; - int right = point.X + xRadius; - int top = point.Y - yRadius; - int bottom = point.Y + yRadius; - - int yIndex = 0; - int xIndex = 0; - - // Faster than clamping + we know we are only looking in one direction - if (left < 0) - { - // Trim the length of our weights iterator across the x-axis. - // Offset our start index across the x-axis. - xIndex = ImageMaths.FastAbs(left); - xLength -= xIndex; - left = 0; - } - - if (top < 0) - { - // Trim the length of our weights iterator across the y-axis. - // Offset our start index across the y-axis. - yIndex = ImageMaths.FastAbs(top); - yLength -= yIndex; - top = 0; - } + float ratio = (float)sourceSize / destinationSize; + float scale = ratio; - if (right >= maxX) + if (scale < 1F) { - // Trim the length of our weights iterator across the x-axis. - xLength -= right - maxX; - } - - if (bottom >= maxY) - { - // Trim the length of our weights iterator across the y-axis. - yLength -= bottom - maxY; - } - - Vector4 result = Vector4.Zero; - - // We calculate our sample by iterating up-down/left-right from our transformed point. - for (int y = top, yi = yIndex; yi < yLength; y++, yi++) - { - float yweight = Unsafe.Add(ref verticalValues, yi); - - for (int x = left, xi = xIndex; xi < xLength; x++, xi++) - { - float xweight = Unsafe.Add(ref horizontalValues, xi); - float weight = yweight * xweight; - - result += source[x, y].ToVector4() * weight; - } + scale = 1F; } - return result; + return (MathF.Ceiling(scale * this.Sampler.Radius), scale); } } diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs index be9de9edaa..9fb1313d9f 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs index 5aab0d07fa..8255af4fe5 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 0.5F; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x > -0.5F && x <= 0.5F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs index 1c84676188..19f466287c 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -15,6 +17,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs index 33435059f1..afc1427f48 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs index 29568db021..3d5af528e2 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs index 492ef69e4c..7e46b05f33 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 3; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs index cae152a53c..d593dbcf43 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 5; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs index b390c55419..5d7c708f2d 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 8; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs index df351d9505..f6e9a9fa09 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0.3333333F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs index 7a7785be36..b1cc8609e7 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 1; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { return x; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs index a345da3f42..9db4b125f2 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0.2620145123990142F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs index ac5e2dedba..815fd9c3dc 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 1F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs index 842da87e06..4b62c767bc 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -14,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 1; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs index acb74a8ec4..9bf19573af 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Processing { /// @@ -13,6 +15,7 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 3; /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) @@ -28,4 +31,4 @@ namespace SixLabors.ImageSharp.Processing return 0F; } } -} +} \ No newline at end of file From 9b8d605b2341af7d31c841545c0e1a87911f13e3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 2 Dec 2017 00:25:02 +1100 Subject: [PATCH 22/99] Performance improvements in loop --- .../Processors/Transforms/AffineProcessor.cs | 38 +++++++++++-------- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 6 +-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index cea7f5953a..3dbcf796fc 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; @@ -109,6 +108,8 @@ namespace SixLabors.ImageSharp.Processing.Processors float yScale = yRadiusScale.scale; float xRadius = xRadiusScale.radius; float yRadius = yRadiusScale.radius; + IResampler sampler = this.Sampler; + var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); Parallel.For( 0, @@ -121,36 +122,41 @@ namespace SixLabors.ImageSharp.Processing.Processors { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. - var point = PointF.Transform(new PointF(x, y), matrix); - int maxX = (int)MathF.Ceiling(point.X + xRadius); - int maxY = (int)MathF.Ceiling(point.Y + yRadius); - int minX = (int)MathF.Floor(point.X - xRadius); - int minY = (int)MathF.Floor(point.Y - yRadius); - - // Clamp sampling pixels to the source image edge - maxX = maxX.Clamp(0, maxSourceX); - minX = minX.Clamp(0, maxSourceX); - maxY = maxY.Clamp(0, maxSourceY); - minY = minY.Clamp(0, maxSourceY); + var point = Vector2.Transform(new Vector2(x, y), matrix); + + // Clamp sampling pixel radial extents to the source image edges + var extents = new Vector4( + MathF.Ceiling(point.X + xRadius), + MathF.Ceiling(point.Y + yRadius), + MathF.Floor(point.X - xRadius), + MathF.Floor(point.Y - yRadius)); + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; if (minX == maxX || minY == maxY) { continue; } - // It appears these have to be calculated manually. + // It appears these have to be calculated on-the-fly. // Using the precalculated weights give the wrong values. - // TODO: Find a way to speed this up. + // TODO: Find a way to speed this up if we can. Vector4 sum = Vector4.Zero; for (int i = minX; i <= maxX; i++) { - float weightX = this.Sampler.GetValue((i - point.X) / xScale); + float weightX = sampler.GetValue((i - point.X) / xScale); for (int j = minY; j <= maxY; j++) { - float weightY = this.Sampler.GetValue((j - point.Y) / yScale); + float weightY = sampler.GetValue((j - point.Y) / yScale); sum += source[i, j].ToVector4() * weightX * weightY; } } + ref TPixel dest = ref destRow[x]; dest.PackFromVector4(sum); } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 2f45e4c83a..eb60aa5fe4 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -17,9 +17,9 @@ - - - + + + From 85afc5f1f1c32ac5818c4843d8e6610b60e2dba7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 3 Dec 2017 00:01:39 +1100 Subject: [PATCH 23/99] Fix inheritance and cleanup --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 27 ++++--- .../Processors/Transforms/AffineProcessor.cs | 79 +++++++++---------- .../Transforms/AutoOrientProcessor.cs | 18 ++--- .../Processors/Transforms/RotateProcessor.cs | 36 ++++----- .../Processors/Transforms/SkewProcessor.cs | 33 +++++--- .../Transforms/Resamplers/BicubicResampler.cs | 3 - .../Transforms/Resamplers/BoxResampler.cs | 3 - .../Resamplers/CatmullRomResampler.cs | 3 - .../Transforms/Resamplers/HermiteResampler.cs | 3 - .../Resamplers/Lanczos2Resampler.cs | 3 - .../Resamplers/Lanczos3Resampler.cs | 3 - .../Resamplers/Lanczos5Resampler.cs | 3 - .../Resamplers/Lanczos8Resampler.cs | 3 - .../Resamplers/MitchellNetravaliResampler.cs | 3 - .../Resamplers/NearestNeighborResampler.cs | 3 - .../Resamplers/RobidouxSharpResampler.cs | 3 - .../Transforms/Resamplers/SplineResampler.cs | 3 - .../Resamplers/TriangleResampler.cs | 3 - .../Transforms/Resamplers/WelchResampler.cs | 3 - .../Processing/Transforms/Rotate.cs | 37 ++------- src/ImageSharp/Processing/Transforms/Skew.cs | 45 +++-------- .../Processing/Transforms/RotateFlipTests.cs | 9 +-- .../Processing/Transforms/RotateTests.cs | 31 ++------ .../Processing/Transforms/SkewTest.cs | 21 +---- 24 files changed, 125 insertions(+), 253 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index fa60f855ee..74d705d53a 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -147,17 +147,26 @@ namespace SixLabors.ImageSharp /// /// The . /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rectangle GetBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) { - var leftTop = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); - var rightTop = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); - var leftBottom = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); - var rightBottom = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); - - Vector2[] allCorners = { leftTop, rightTop, leftBottom, rightBottom }; - float extentX = allCorners.Select(v => v.X).Max() - allCorners.Select(v => v.X).Min(); - float extentY = allCorners.Select(v => v.Y).Max() - allCorners.Select(v => v.Y).Min(); - return new Rectangle(0, 0, (int)MathF.Ceiling(extentX), (int)MathF.Ceiling(extentY)); + // Calculate the position of the four corners in world space by applying + // The world matrix to the four corners in object space (0, 0, width, height) + var tl = Vector2.Transform(Vector2.Zero, matrix); + var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); + var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); + var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); + + // Find the minimum and maximum "corners" based on the ones above + float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); + float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); + float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); + float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); + var min = new Vector2(minX, minY); + var max = new Vector2(maxX, maxY); + Vector2 size = max - min; + + return new Rectangle((int)MathF.Floor(minX), (int)MathF.Floor(minY), (int)MathF.Ceiling(size.X), (int)MathF.Ceiling(size.Y)); } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 3dbcf796fc..843d07f4b5 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Threading.Tasks; @@ -18,52 +17,40 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides the base methods to perform affine transforms on an image. /// /// The pixel format. - internal abstract class AffineProcessor : ResamplingWeightedProcessor + internal abstract class AffineProcessor : CloningImageProcessor where TPixel : struct, IPixel { + private Rectangle targetRectangle; + private Matrix3x2 transformMatrix; + /// /// Initializes a new instance of the class. /// /// The sampler to perform the resize operation. protected AffineProcessor(IResampler sampler) - : base(sampler, 1, 1, Rectangles.DefaultRectangle) // Hack to prevent Guard throwing in base, we always set the canvas { + this.Sampler = sampler; } /// - /// Gets or sets a value indicating whether to expand the canvas to fit the transformed image. + /// Gets the sampler to perform interpolation of the transform operation. /// - public bool Expand { get; set; } = true; + public IResampler Sampler { get; } /// /// Returns the processing matrix used for transforming the image. /// - /// The rectangle bounds /// The - protected abstract Matrix3x2 CreateProcessingMatrix(Rectangle rectangle); - - /// - /// Creates a new target canvas to contain the results of the matrix transform. - /// - /// The source rectangle. - protected virtual void CreateNewCanvas(Rectangle sourceRectangle) - { - this.ResizeRectangle = Matrix3x2.Invert(this.CreateProcessingMatrix(sourceRectangle), out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; - - this.Width = this.ResizeRectangle.Width; - this.Height = this.ResizeRectangle.Height; - } + protected abstract Matrix3x2 GetTransformMatrix(); /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - this.CreateNewCanvas(sourceRectangle); + this.ResizeCanvas(sourceRectangle); // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(this.ResizeRectangle.Width, this.ResizeRectangle.Height, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); @@ -72,9 +59,11 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { - int height = this.ResizeRectangle.Height; - int width = this.ResizeRectangle.Width; + int height = this.targetRectangle.Height; + int width = this.targetRectangle.Width; Rectangle sourceBounds = source.Bounds(); + + // Since could potentially be resizing the canvas we need to recenter the matrix Matrix3x2 matrix = this.GetCenteredMatrix(source); if (this.Sampler is NearestNeighborResampler) @@ -106,8 +95,7 @@ namespace SixLabors.ImageSharp.Processing.Processors (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); float xScale = xRadiusScale.scale; float yScale = yRadiusScale.scale; - float xRadius = xRadiusScale.radius; - float yRadius = yRadiusScale.radius; + var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); IResampler sampler = this.Sampler; var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); @@ -125,11 +113,14 @@ namespace SixLabors.ImageSharp.Processing.Processors var point = Vector2.Transform(new Vector2(x, y), matrix); // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; + var extents = new Vector4( - MathF.Ceiling(point.X + xRadius), - MathF.Ceiling(point.Y + yRadius), - MathF.Floor(point.X - xRadius), - MathF.Floor(point.Y - yRadius)); + MathF.Ceiling(maxXY.X), + MathF.Ceiling(maxXY.Y), + MathF.Floor(minXY.X), + MathF.Floor(minXY.Y)); extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); @@ -172,9 +163,21 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected Matrix3x2 GetCenteredMatrix(ImageFrame source) { - var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.ResizeRectangle.Width * .5F, -this.ResizeRectangle.Height * .5F); + var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.targetRectangle.Width * .5F, -this.targetRectangle.Height * .5F); var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return (translationToTargetCenter * this.CreateProcessingMatrix(this.ResizeRectangle)) * translateToSourceCenter; + return translationToTargetCenter * this.transformMatrix * translateToSourceCenter; + } + + /// + /// Creates a new target canvas to contain the results of the matrix transform. + /// + /// The source rectangle. + private void ResizeCanvas(Rectangle sourceRectangle) + { + this.transformMatrix = this.GetTransformMatrix(); + this.targetRectangle = Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 sizeMatrix) + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; } /// @@ -196,14 +199,4 @@ namespace SixLabors.ImageSharp.Processing.Processors return (MathF.Ceiling(scale * this.Sampler.Radius), scale); } } - - /// - /// Contains a static rectangle used for comparison when creating a new canvas. - /// We do this so we can inherit from the resampling weights class and pass the guard in the constructor and also avoid creating a new rectangle each time. - /// - [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "I'm using this only here to prevent duplication in generic types.")] - internal static class Rectangles - { - public static Rectangle DefaultRectangle { get; } = new Rectangle(0, 0, 1, 1); - } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs index ab93e0e384..c39311bc33 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs @@ -15,13 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class AutoOrientProcessor : ImageProcessor where TPixel : struct, IPixel { - /// - /// Initializes a new instance of the class. - /// - public AutoOrientProcessor() - { - } - + /// protected override void BeforeImageApply(Image source, Rectangle sourceRectangle) { Orientation orientation = GetExifOrientation(source); @@ -33,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors break; case Orientation.BottomRight: - new RotateProcessor() { Angle = (int)RotateType.Rotate180, Expand = false }.Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate180).Apply(source, sourceRectangle); break; case Orientation.BottomLeft: @@ -41,21 +35,21 @@ namespace SixLabors.ImageSharp.Processing.Processors break; case Orientation.LeftTop: - new RotateProcessor() { Angle = (int)RotateType.Rotate90, Expand = false }.Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate90).Apply(source, sourceRectangle); new FlipProcessor(FlipType.Horizontal).Apply(source, sourceRectangle); break; case Orientation.RightTop: - new RotateProcessor() { Angle = (int)RotateType.Rotate90, Expand = false }.Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate90).Apply(source, sourceRectangle); break; case Orientation.RightBottom: new FlipProcessor(FlipType.Vertical).Apply(source, sourceRectangle); - new RotateProcessor() { Angle = (int)RotateType.Rotate270, Expand = false }.Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate270).Apply(source, sourceRectangle); break; case Orientation.LeftBottom: - new RotateProcessor() { Angle = (int)RotateType.Rotate270, Expand = false }.Apply(source, sourceRectangle); + new RotateProcessor((int)RotateType.Rotate270).Apply(source, sourceRectangle); break; case Orientation.Unknown: diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 1ad7263929..be49ec321b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -19,39 +19,35 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class RotateProcessor : AffineProcessor where TPixel : struct, IPixel { - private Matrix3x2 transformMatrix; - /// /// Initializes a new instance of the class. /// - public RotateProcessor() - : base(KnownResamplers.NearestNeighbor) + /// The angle of rotation in degrees. + public RotateProcessor(float angle) + : this(angle, KnownResamplers.NearestNeighbor) { } /// /// Initializes a new instance of the class. /// + /// The angle of rotation in degrees. /// The sampler to perform the rotating operation. - public RotateProcessor(IResampler sampler) + public RotateProcessor(float degrees, IResampler sampler) : base(sampler) { + this.Degrees = degrees; } /// - /// Gets or sets the angle of processMatrix in degrees. + /// Gets the angle of rotation in degrees. /// - public float Angle { get; set; } + public float Degrees { get; } /// - protected override Matrix3x2 CreateProcessingMatrix(Rectangle rectangle) + protected override Matrix3x2 GetTransformMatrix() { - if (this.transformMatrix == default(Matrix3x2)) - { - this.transformMatrix = Matrix3x2Extensions.CreateRotationDegrees(-this.Angle, PointF.Empty); - } - - return this.transformMatrix; + return Matrix3x2Extensions.CreateRotationDegrees(-this.Degrees, PointF.Empty); } /// @@ -74,7 +70,7 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - if (MathF.Abs(this.Angle) < Constants.Epsilon) + if (MathF.Abs(this.Degrees) < Constants.Epsilon) { // No need to do anything so return. return; @@ -82,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors profile.RemoveValue(ExifTag.Orientation); - if (this.Expand && profile.GetValue(ExifTag.PixelXDimension) != null) + if (profile.GetValue(ExifTag.PixelXDimension) != null) { profile.SetValue(ExifTag.PixelXDimension, source.Width); profile.SetValue(ExifTag.PixelYDimension, source.Height); @@ -100,26 +96,26 @@ namespace SixLabors.ImageSharp.Processing.Processors /// private bool OptimizedApply(ImageFrame source, ImageFrame destination, Configuration configuration) { - if (MathF.Abs(this.Angle) < Constants.Epsilon) + if (MathF.Abs(this.Degrees) < Constants.Epsilon) { // The destination will be blank here so copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return true; } - if (MathF.Abs(this.Angle - 90) < Constants.Epsilon) + if (MathF.Abs(this.Degrees - 90) < Constants.Epsilon) { this.Rotate90(source, destination, configuration); return true; } - if (MathF.Abs(this.Angle - 180) < Constants.Epsilon) + if (MathF.Abs(this.Degrees - 180) < Constants.Epsilon) { this.Rotate180(source, destination, configuration); return true; } - if (MathF.Abs(this.Angle - 270) < Constants.Epsilon) + if (MathF.Abs(this.Degrees - 270) < Constants.Epsilon) { this.Rotate270(source, destination, configuration); return true; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 61526747ea..8da8b1e57b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -14,36 +14,43 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class SkewProcessor : AffineProcessor where TPixel : struct, IPixel { - private Matrix3x2 transformMatrix; + /// + /// Initializes a new instance of the class. + /// + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. + public SkewProcessor(float degreesX, float degreesY) + : this(degreesX, degreesY, KnownResamplers.NearestNeighbor) + { + } /// /// Initializes a new instance of the class. /// + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. /// The sampler to perform the skew operation. - public SkewProcessor(IResampler sampler) + public SkewProcessor(float degreesX, float degreesY, IResampler sampler) : base(sampler) { + this.DegreesX = degreesX; + this.DegreesY = degreesY; } /// - /// Gets or sets the angle of rotation along the x-axis in degrees. + /// Gets the angle of rotation along the x-axis in degrees. /// - public float AngleX { get; set; } + public float DegreesX { get; } /// - /// Gets or sets the angle of rotation along the y-axis in degrees. + /// Gets the angle of rotation along the y-axis in degrees. /// - public float AngleY { get; set; } + public float DegreesY { get; } /// - protected override Matrix3x2 CreateProcessingMatrix(Rectangle rectangle) + protected override Matrix3x2 GetTransformMatrix() { - if (this.transformMatrix == default(Matrix3x2)) - { - this.transformMatrix = Matrix3x2Extensions.CreateSkewDegrees(-this.AngleX, -this.AngleY, PointF.Empty); - } - - return this.transformMatrix; + return Matrix3x2Extensions.CreateSkewDegrees(-this.DegreesX, -this.DegreesY, PointF.Empty); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs index 9fb1313d9f..be9de9edaa 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/BicubicResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs index 8255af4fe5..5aab0d07fa 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/BoxResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 0.5F; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x > -0.5F && x <= 0.5F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs index 19f466287c..1c84676188 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/CatmullRomResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -17,7 +15,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs index afc1427f48..33435059f1 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/HermiteResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs index 3d5af528e2..29568db021 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos2Resampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs index 7e46b05f33..492ef69e4c 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos3Resampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 3; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs index d593dbcf43..cae152a53c 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos5Resampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 5; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs index 5d7c708f2d..b390c55419 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/Lanczos8Resampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 8; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs index f6e9a9fa09..df351d9505 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/MitchellNetravaliResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0.3333333F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs index b1cc8609e7..7a7785be36 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/NearestNeighborResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 1; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { return x; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs index 9db4b125f2..a345da3f42 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/RobidouxSharpResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 0.2620145123990142F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs index 815fd9c3dc..ac5e2dedba 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/SplineResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 2; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { const float B = 1F; diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs index 4b62c767bc..842da87e06 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/TriangleResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -16,7 +14,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 1; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs b/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs index 9bf19573af..9e18a24710 100644 --- a/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs +++ b/src/ImageSharp/Processing/Transforms/Resamplers/WelchResampler.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Processing { /// @@ -15,7 +13,6 @@ namespace SixLabors.ImageSharp.Processing public float Radius => 3; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetValue(float x) { if (x < 0F) diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index 2ec1385bb9..e9ae4fcf32 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -12,29 +12,6 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { - /// - /// Rotates an image by the given angle in degrees, expanding the image to fit the rotated result. - /// - /// The pixel format. - /// The image to rotate. - /// The angle in degrees to perform the rotation. - /// The - public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees) - where TPixel : struct, IPixel - => Rotate(source, degrees, true); - - /// - /// Rotates an image by the given angle in degrees, expanding the image to fit the rotated result. - /// - /// The pixel format. - /// The image to rotate. - /// The angle in degrees to perform the rotation. - /// The to perform the resampling. - /// The - public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler) - where TPixel : struct, IPixel - => Rotate(source, degrees, sampler, true); - /// /// Rotates and flips an image by the given instructions. /// @@ -44,7 +21,7 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, RotateType rotateType) where TPixel : struct, IPixel - => Rotate(source, (float)rotateType, false); + => Rotate(source, (float)rotateType); /// /// Rotates an image by the given angle in degrees. @@ -52,11 +29,10 @@ namespace SixLabors.ImageSharp /// The pixel format. /// The image to rotate. /// The angle in degrees to perform the rotation. - /// Whether to expand the image to fit the rotated result. /// The - public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, bool expand) + public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees) where TPixel : struct, IPixel - => Rotate(source, degrees, KnownResamplers.NearestNeighbor, expand); + => Rotate(source, degrees, KnownResamplers.NearestNeighbor); /// /// Rotates an image by the given angle in degrees using the specified sampling algorithm. @@ -65,10 +41,9 @@ namespace SixLabors.ImageSharp /// The image to rotate. /// The angle in degrees to perform the rotation. /// The to perform the resampling. - /// Whether to expand the image to fit the rotated result. /// The - public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler, bool expand) + public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new RotateProcessor(sampler) { Angle = degrees, Expand = expand }); + => source.ApplyProcessor(new RotateProcessor(degrees, sampler)); } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index f03e60e3da..b7a431cce4 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -12,56 +12,29 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { - /// - /// Skews an image by the given angles in degrees, expanding the image to fit the skewed result. - /// - /// The pixel format. - /// The image to skew. - /// The angle in degrees to perform the rotation along the x-axis. - /// The angle in degrees to perform the rotation along the y-axis. - /// The - public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY) - where TPixel : struct, IPixel - => Skew(source, degreesX, degreesY, true); - - /// - /// Skews an image by the given angles in degrees using the given sampler, expanding the image to fit the skewed result. - /// - /// The pixel format. - /// The image to skew. - /// The angle in degrees to perform the rotation along the x-axis. - /// The angle in degrees to perform the rotation along the y-axis. - /// The to perform the resampling. - /// The - public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler) - where TPixel : struct, IPixel - => Skew(source, degreesX, degreesY, sampler, true); - /// /// Skews an image by the given angles in degrees. /// /// The pixel format. /// The image to skew. - /// The angle in degrees to perform the rotation along the x-axis. - /// The angle in degrees to perform the rotation along the y-axis. - /// Whether to expand the image to fit the skewed result. + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. /// The - public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, bool expand) + public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY) where TPixel : struct, IPixel - => Skew(source, degreesX, degreesY, KnownResamplers.NearestNeighbor, expand); + => Skew(source, degreesX, degreesY, KnownResamplers.NearestNeighbor); /// /// Skews an image by the given angles in degrees using the specified sampling algorithm. /// /// The pixel format. /// The image to skew. - /// The angle in degrees to perform the rotation along the x-axis. - /// The angle in degrees to perform the rotation along the y-axis. + /// The angle in degrees to perform the skew along the x-axis. + /// The angle in degrees to perform the skew along the y-axis. /// The to perform the resampling. - /// Whether to expand the image to fit the skewed result. /// The - public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler, bool expand) + public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new SkewProcessor(sampler) { AngleX = degreesX, AngleY = degreesY, Expand = expand }); + => source.ApplyProcessor(new SkewProcessor(degreesX, degreesY, sampler)); } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs index 67602131b0..75d7067702 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/RotateFlipTests.cs @@ -25,14 +25,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms [InlineData(RotateType.Rotate90, FlipType.Vertical, 90)] [InlineData(RotateType.Rotate180, FlipType.Vertical, 180)] [InlineData(RotateType.Rotate270, FlipType.Vertical, 270)] - public void Rotate_degreesFloat_RotateProcessorWithAnglesSetAndExpandTrue(RotateType angle, FlipType flip, float expectedAngle) + public void Rotate_degreesFloat_RotateProcessorWithAnglesSetrue(RotateType angle, FlipType flip, float expectedAngle) { this.operations.RotateFlip(angle, flip); - var rotateProcessor = this.Verify>(0); - var flipProcessor = this.Verify>(1); + RotateProcessor rotateProcessor = this.Verify>(0); + FlipProcessor flipProcessor = this.Verify>(1); - Assert.Equal(expectedAngle, rotateProcessor.Angle); - Assert.False(rotateProcessor.Expand); + Assert.Equal(expectedAngle, rotateProcessor.Degrees); Assert.Equal(flip, flipProcessor.FlipType); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs index 80eccd00cd..a990fa88ca 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/RotateTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; using Xunit; @@ -10,46 +9,28 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class RotateTests : BaseImageOperationsExtensionTest { - [Theory] [InlineData(85.6f)] [InlineData(21)] - public void Rotate_degreesFloat_RotateProcessorWithAnglesSetAndExpandTrue(float angle) + public void RotateDegreesFloatRotateProcessorWithAnglesSet(float angle) { this.operations.Rotate(angle); - var processor = this.Verify>(); + RotateProcessor processor = this.Verify>(); - Assert.Equal(angle, processor.Angle); - Assert.True(processor.Expand); + Assert.Equal(angle, processor.Degrees); } - [Theory] [InlineData(RotateType.None, 0)] [InlineData(RotateType.Rotate90, 90)] [InlineData(RotateType.Rotate180, 180)] [InlineData(RotateType.Rotate270, 270)] - public void Rotate_RotateType_RotateProcessorWithAnglesConvertedFromEnumAndExpandTrue(RotateType angle, float expectedangle) + public void RotateRotateTypeRotateProcessorWithAnglesConvertedFromEnum(RotateType angle, float expectedangle) { this.operations.Rotate(angle); // is this api needed ??? - var processor = this.Verify>(); - - Assert.Equal(expectedangle, processor.Angle); - Assert.False(processor.Expand); - } - - - [Theory] - [InlineData(85.6f, false)] - [InlineData(21, true)] - [InlineData(21, false)] - public void Rotate_degreesFloat_expand_RotateProcessorWithAnglesSetAndExpandSet(float angle, bool expand) - { - this.operations.Rotate(angle, expand); - var processor = this.Verify>(); + RotateProcessor processor = this.Verify>(); - Assert.Equal(angle, processor.Angle); - Assert.Equal(expand, processor.Expand); + Assert.Equal(expectedangle, processor.Degrees); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs index 28a0e0d8f8..d2cc5764ed 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/SkewTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; using Xunit; @@ -10,25 +9,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public class SkewTest : BaseImageOperationsExtensionTest { [Fact] - public void Skew_x_y_CreateSkewProcessorWithAnglesSetAndExpandTrue() + public void SkewXYCreateSkewProcessorWithAnglesSet() { this.operations.Skew(10, 20); - var processor = this.Verify>(); + SkewProcessor processor = this.Verify>(); - Assert.Equal(10, processor.AngleX); - Assert.Equal(20, processor.AngleY); - Assert.True(processor.Expand); - } - - [Fact] - public void Skew_x_y_expand_CreateSkewProcessorWithAnglesSetAndExpandTrue() - { - this.operations.Skew(10, 20, false); - var processor = this.Verify>(); - - Assert.Equal(10, processor.AngleX); - Assert.Equal(20, processor.AngleY); - Assert.False(processor.Expand); + Assert.Equal(10, processor.DegreesX); + Assert.Equal(20, processor.DegreesY); } } } \ No newline at end of file From 7319986994e41f729ddca436307ba4a8e1234204 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 3 Dec 2017 03:36:49 +1100 Subject: [PATCH 24/99] WIP make this work with scaled-down transforms. If we normalize the weights to make this work when the output is scaled down we break the edge pixel output. Somehow fix this. --- .../Processors/Transforms/AffineProcessor.cs | 125 ++++++++++++------ .../Transforms/TransformProcessor.cs | 47 +++++++ .../Processing/Transforms/Transform.cs | 39 ++++++ .../Processing/Transforms/TransformTests.cs | 79 +++++++++++ 4 files changed, 249 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs create mode 100644 src/ImageSharp/Processing/Transforms/Transform.cs create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 843d07f4b5..26b8e3a124 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -26,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// Initializes a new instance of the class. /// - /// The sampler to perform the resize operation. + /// The sampler to perform the transform operation. protected AffineProcessor(IResampler sampler) { this.Sampler = sampler; @@ -91,13 +92,15 @@ namespace SixLabors.ImageSharp.Processing.Processors int maxSourceX = source.Width - 1; int maxSourceY = source.Height - 1; - (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); - (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + (float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + (float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); float xScale = xRadiusScale.scale; float yScale = yRadiusScale.scale; var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); IResampler sampler = this.Sampler; var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); + int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); + int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); Parallel.For( 0, @@ -106,50 +109,90 @@ namespace SixLabors.ImageSharp.Processing.Processors y => { Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) + using (var yBuffer = new Buffer(yLength)) + using (var xBuffer = new Buffer(xLength)) { - // Use the single precision position to calculate correct bounding pixels - // otherwise we get rogue pixels outside of the bounds. - var point = Vector2.Transform(new Vector2(x, y), matrix); - - // Clamp sampling pixel radial extents to the source image edges - Vector2 maxXY = point + radius; - Vector2 minXY = point - radius; - - var extents = new Vector4( - MathF.Ceiling(maxXY.X), - MathF.Ceiling(maxXY.Y), - MathF.Floor(minXY.X), - MathF.Floor(minXY.Y)); + for (int x = 0; x < width; x++) + { + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var point = Vector2.Transform(new Vector2(x, y), matrix); + + // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; + + var extents = new Vector4( + MathF.Ceiling(maxXY.X), + MathF.Ceiling(maxXY.Y), + MathF.Floor(minXY.X), + MathF.Floor(minXY.Y)); + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; + + // TODO: Find a way to speed this up if we can we precalculated weights!!! + // It appears these have to be calculated on-the-fly. + // Check with Anton to figure out why indexing from the precalculated weights was wrong. + // + // Create and normalize the y-weights + float ySum = 0; + for (int yy = 0, i = minY; i <= maxY; i++, yy++) + { + float weight = sampler.GetValue((i - point.Y) / yScale); + ySum += weight; + yBuffer[yy] = weight; + } - extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + // TODO: + // Normalizing the weights fixes scaled transfrom where we scale down but breaks edge pixel belnding + // We end up with too much weight on pixels that should be blended. + // We could, maybe, move the division into the loop and not divide when we hit 0 or maxN but that seems clunky. + if (ySum > 0) + { + for (int i = 0; i < yBuffer.Length; i++) + { + yBuffer[i] = yBuffer[i] / ySum; + } + } - int maxX = (int)extents.X; - int maxY = (int)extents.Y; - int minX = (int)extents.Z; - int minY = (int)extents.W; + // Create and normalize the x-weights + float xSum = 0; + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float weight = sampler.GetValue((i - point.X) / xScale); + xSum += weight; + xBuffer[xx] = weight; + } - if (minX == maxX || minY == maxY) - { - continue; - } + if (xSum > 0) + { + for (int i = 0; i < xBuffer.Length; i++) + { + xBuffer[i] = xBuffer[i] / xSum; + } + } - // It appears these have to be calculated on-the-fly. - // Using the precalculated weights give the wrong values. - // TODO: Find a way to speed this up if we can. - Vector4 sum = Vector4.Zero; - for (int i = minX; i <= maxX; i++) - { - float weightX = sampler.GetValue((i - point.X) / xScale); - for (int j = minY; j <= maxY; j++) + // Now multiply the normalized results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float weightY = sampler.GetValue((j - point.Y) / yScale); - sum += source[i, j].ToVector4() * weightX * weightY; + float yWeight = yBuffer[yy]; + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = xBuffer[xx]; + sum += source[i, j].ToVector4() * xWeight * yWeight; + } } - } - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(sum); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(sum); + } } }); } @@ -186,7 +229,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source dimension size /// The destination dimension size /// The radius, and scaling factor - private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + private (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) { float ratio = (float)sourceSize / destinationSize; float scale = ratio; @@ -196,7 +239,7 @@ namespace SixLabors.ImageSharp.Processing.Processors scale = 1F; } - return (MathF.Ceiling(scale * this.Sampler.Radius), scale); + return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs new file mode 100644 index 0000000000..62bdc4a1eb --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides methods that allow the tranformation of images using various algorithms. + /// + /// The pixel format. + internal class TransformProcessor : AffineProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transformation matrix + public TransformProcessor(Matrix3x2 matrix) + : this(matrix, KnownResamplers.NearestNeighbor) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The transformation matrix + /// The sampler to perform the transform operation. + public TransformProcessor(Matrix3x2 matrix, IResampler sampler) + : base(sampler) + { + this.TransformMatrix = matrix; + } + + /// + /// Gets the transform matrix + /// + public Matrix3x2 TransformMatrix { get; } + + /// + protected override Matrix3x2 GetTransformMatrix() + { + return this.TransformMatrix; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs new file mode 100644 index 0000000000..f3478e32d5 --- /dev/null +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Transforms an image by the given matrix. + /// + /// The pixel format. + /// The image to skew. + /// The transformation matrix. + /// The + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix) + where TPixel : struct, IPixel + => Transform(source, matrix, KnownResamplers.NearestNeighbor); + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to skew. + /// The transformation matrix. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) + where TPixel : struct, IPixel + => source.ApplyProcessor(new TransformProcessor(matrix, sampler)); + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs new file mode 100644 index 0000000000..30cb626155 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + using System.Numerics; + using System.Reflection; + + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; + using SixLabors.Primitives; + + using Xunit; + + public class TransformTests : FileTestBase + { + public static readonly TheoryData TransformValues + = new TheoryData + { + { 20, 10, 50 }, + { -20, -10, 50 } + }; + + public static readonly List ResamplerNames + = new List + { + nameof(KnownResamplers.Bicubic), + //nameof(KnownResamplers.Box), + //nameof(KnownResamplers.CatmullRom), + //nameof(KnownResamplers.Hermite), + //nameof(KnownResamplers.Lanczos2), + //nameof(KnownResamplers.Lanczos3), + //nameof(KnownResamplers.Lanczos5), + //nameof(KnownResamplers.Lanczos8), + //nameof(KnownResamplers.MitchellNetravali), + //nameof(KnownResamplers.NearestNeighbor), + //nameof(KnownResamplers.Robidoux), + //nameof(KnownResamplers.RobidouxSharp), + //nameof(KnownResamplers.Spline), + //nameof(KnownResamplers.Triangle), + //nameof(KnownResamplers.Welch), + }; + + [Theory] + [WithFileCollection(nameof(DefaultFiles), nameof(TransformValues), DefaultPixelType)] + public void ImageShouldTransformWithSampler(TestImageProvider provider, float x, float y, float z) + where TPixel : struct, IPixel + { + + foreach (string resamplerName in ResamplerNames) + { + IResampler sampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(-z); + + // TODO, how does scale work? 2 means half just now, + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F)); + + + image.Mutate(i => i.Transform(scale * rotate, sampler)); + image.DebugSave(provider, string.Join("_", x, y, resamplerName)); + } + } + } + + private static IResampler GetResampler(string name) + { + PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); + + if (property == null) + { + throw new Exception("Invalid property name!"); + } + + return (IResampler)property.GetValue(null); + } + } +} \ No newline at end of file From 36d4fa6aab574fcea163fb2b8fa949c17c6d029b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Dec 2017 12:37:33 +1100 Subject: [PATCH 25/99] Handle downsized transforms --- .../Processors/Transforms/AffineProcessor.cs | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 26b8e3a124..f7ba651778 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; @@ -128,6 +129,11 @@ namespace SixLabors.ImageSharp.Processing.Processors MathF.Floor(minXY.X), MathF.Floor(minXY.Y)); + int right = (int)extents.X; + int bottom = (int)extents.Y; + int left = (int)extents.Z; + int top = (int)extents.W; + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); int maxX = (int)extents.X; @@ -135,46 +141,34 @@ namespace SixLabors.ImageSharp.Processing.Processors int minX = (int)extents.Z; int minY = (int)extents.W; + if (minX == maxX || minY == maxY) + { + continue; + } + // TODO: Find a way to speed this up if we can we precalculated weights!!! // It appears these have to be calculated on-the-fly. // Check with Anton to figure out why indexing from the precalculated weights was wrong. + // It might not be possible to do so with the resizer weights but perhaps we can fashion something similar for here. // // Create and normalize the y-weights - float ySum = 0; - for (int yy = 0, i = minY; i <= maxY; i++, yy++) + if (yScale > 1) { - float weight = sampler.GetValue((i - point.Y) / yScale); - ySum += weight; - yBuffer[yy] = weight; + CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, yBuffer); } - - // TODO: - // Normalizing the weights fixes scaled transfrom where we scale down but breaks edge pixel belnding - // We end up with too much weight on pixels that should be blended. - // We could, maybe, move the division into the loop and not divide when we hit 0 or maxN but that seems clunky. - if (ySum > 0) + else { - for (int i = 0; i < yBuffer.Length; i++) - { - yBuffer[i] = yBuffer[i] / ySum; - } + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, yBuffer); } // Create and normalize the x-weights - float xSum = 0; - for (int xx = 0, i = minX; i <= maxX; i++, xx++) + if (xScale > 1) { - float weight = sampler.GetValue((i - point.X) / xScale); - xSum += weight; - xBuffer[xx] = weight; + CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xBuffer); } - - if (xSum > 0) + else { - for (int i = 0; i < xBuffer.Length; i++) - { - xBuffer[i] = xBuffer[i] / xSum; - } + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xBuffer); } // Now multiply the normalized results against the offsets @@ -211,6 +205,52 @@ namespace SixLabors.ImageSharp.Processing.Processors return translationToTargetCenter * this.transformMatrix * translateToSourceCenter; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CalculateWeightsDown(int top, int bottom, int min, int max, float point, IResampler sampler, float scale, Buffer weights) + { + float sum = 0; + ref float weightsBaseRef = ref weights[0]; + + // Downsampling weights requires more edge sampling plus normalization of the weights + for (int x = 0, i = top; i <= bottom; i++, x++) + { + int index = i; + if (index < min) + { + index = min; + } + + if (index > max) + { + index = max; + } + + float weight = sampler.GetValue((index - point) / scale); + sum += weight; + Unsafe.Add(ref weightsBaseRef, x) = weight; + } + + if (sum > 0) + { + for (int i = 0; i < weights.Length; i++) + { + ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i); + wRef = wRef / sum; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CalculateWeightsScaleUp(int min, int max, float point, IResampler sampler, Buffer weights) + { + ref float weightsBaseRef = ref weights[0]; + for (int x = 0, i = min; i <= max; i++, x++) + { + float weight = sampler.GetValue(i - point); + Unsafe.Add(ref weightsBaseRef, x) = weight; + } + } + /// /// Creates a new target canvas to contain the results of the matrix transform. /// @@ -218,9 +258,11 @@ namespace SixLabors.ImageSharp.Processing.Processors private void ResizeCanvas(Rectangle sourceRectangle) { this.transformMatrix = this.GetTransformMatrix(); + + // this.targetRectangle = ImageMaths.GetBoundingRectangle(sourceRectangle, this.transformMatrix); this.targetRectangle = Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; } /// From 70ec64752438a70a4262d93f8a90ef07c0b762d6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Dec 2017 15:33:10 +1100 Subject: [PATCH 26/99] Normalize matrix transforms across methods Methods now match w3c expected results. --- .../Processors/Transforms/AffineProcessor.cs | 52 +++-- .../ResamplingWeightedProcessor.Weights.cs | 199 ------------------ .../Transforms/ResamplingWeightedProcessor.cs | 2 +- .../Processors/Transforms/RotateProcessor.cs | 4 +- .../Processors/Transforms/SkewProcessor.cs | 4 +- .../Transforms/TransformProcessor.cs | 1 + .../Processors/Transforms/WeightsBuffer.cs | 53 +++++ .../Processors/Transforms/WeightsWindow.cs | 150 +++++++++++++ .../Transforms/ResizeProfilingBenchmarks.cs | 4 +- .../Processing/Transforms/TransformTests.cs | 11 +- 10 files changed, 253 insertions(+), 227 deletions(-) delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index f7ba651778..767c6feed1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int width = this.targetRectangle.Width; Rectangle sourceBounds = source.Bounds(); - // Since could potentially be resizing the canvas we need to recenter the matrix + // Since could potentially be resizing the canvas we need to re-center the matrix Matrix3x2 matrix = this.GetCenteredMatrix(source); if (this.Sampler is NearestNeighborResampler) @@ -146,10 +146,10 @@ namespace SixLabors.ImageSharp.Processing.Processors continue; } - // TODO: Find a way to speed this up if we can we precalculated weights!!! // It appears these have to be calculated on-the-fly. - // Check with Anton to figure out why indexing from the precalculated weights was wrong. - // It might not be possible to do so with the resizer weights but perhaps we can fashion something similar for here. + // Precalulating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions. + // I've optimized where I can but am always open to suggestions. // // Create and normalize the y-weights if (yScale > 1) @@ -171,7 +171,7 @@ namespace SixLabors.ImageSharp.Processing.Processors CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xBuffer); } - // Now multiply the normalized results against the offsets + // Now multiply the results against the offsets Vector4 sum = Vector4.Zero; for (int yy = 0, j = minY; j <= maxY; j++, yy++) { @@ -205,24 +205,36 @@ namespace SixLabors.ImageSharp.Processing.Processors return translationToTargetCenter * this.transformMatrix * translateToSourceCenter; } + /// + /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Additionally the weights are nomalized. + /// + /// The minimum sampling offset + /// The maximum sampling offset + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The transformed image scale relative to the source + /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsDown(int top, int bottom, int min, int max, float point, IResampler sampler, float scale, Buffer weights) + private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Buffer weights) { float sum = 0; ref float weightsBaseRef = ref weights[0]; // Downsampling weights requires more edge sampling plus normalization of the weights - for (int x = 0, i = top; i <= bottom; i++, x++) + for (int x = 0, i = min; i <= max; i++, x++) { int index = i; - if (index < min) + if (index < sourceMin) { - index = min; + index = sourceMin; } - if (index > max) + if (index > sourceMax) { - index = max; + index = sourceMax; } float weight = sampler.GetValue((index - point) / scale); @@ -240,11 +252,20 @@ namespace SixLabors.ImageSharp.Processing.Processors } } + /// + /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Additionally the weights are nomalized. + /// + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsScaleUp(int min, int max, float point, IResampler sampler, Buffer weights) + private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Buffer weights) { ref float weightsBaseRef = ref weights[0]; - for (int x = 0, i = min; i <= max; i++, x++) + for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) { float weight = sampler.GetValue(i - point); Unsafe.Add(ref weightsBaseRef, x) = weight; @@ -259,10 +280,9 @@ namespace SixLabors.ImageSharp.Processing.Processors { this.transformMatrix = this.GetTransformMatrix(); - // this.targetRectangle = ImageMaths.GetBoundingRectangle(sourceRectangle, this.transformMatrix); this.targetRectangle = Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs deleted file mode 100644 index 1169d2eadc..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Conains the definition of and . - /// - internal abstract partial class ResamplingWeightedProcessor - { - /// - /// Points to a collection of of weights allocated in . - /// - internal struct WeightsWindow - { - /// - /// The local left index position - /// - public int Left; - - /// - /// The length of the weights window - /// - public int Length; - - /// - /// The index in the destination buffer - /// - private readonly int flatStartIndex; - - /// - /// The buffer containing the weights values. - /// - private readonly Buffer buffer; - - /// - /// Initializes a new instance of the struct. - /// - /// The destination index in the buffer - /// The local left index - /// The span - /// The length of the window - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal WeightsWindow(int index, int left, Buffer2D buffer, int length) - { - this.flatStartIndex = (index * buffer.Width) + left; - this.Left = left; - this.buffer = buffer; - this.Length = length; - } - - /// - /// Gets a reference to the first item of the window. - /// - /// The reference to the first item of the window - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref float GetStartReference() - { - return ref this.buffer[this.flatStartIndex]; - } - - /// - /// Gets the span representing the portion of the that this window covers - /// - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length); - - /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. - /// - /// The input span of vectors - /// The source row position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedRowSum(Span rowSpan, int sourceX) - { - ref float horizontalValues = ref this.GetStartReference(); - int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v * weight; - } - - return result; - } - - /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. - /// Applies to all input vectors. - /// - /// The input span of vectors - /// The source row position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeExpandedWeightedRowSum(Span rowSpan, int sourceX) - { - ref float horizontalValues = ref this.GetStartReference(); - int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v.Expand() * weight; - } - - return result; - } - - /// - /// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x', - /// weighted by weight values, pointed by this instance. - /// - /// The buffer of input vectors in row first order - /// The row position - /// The source column position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedColumnSum(Buffer2D firstPassPixels, int x, int sourceY) - { - ref float verticalValues = ref this.GetStartReference(); - int left = this.Left; - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float yw = Unsafe.Add(ref verticalValues, i); - int index = left + i + sourceY; - result += firstPassPixels[x, index] * yw; - } - - return result; - } - } - - /// - /// Holds the values in an optimized contigous memory region. - /// - internal class WeightsBuffer : IDisposable - { - private readonly Buffer2D dataBuffer; - - /// - /// Initializes a new instance of the class. - /// - /// The size of the source window - /// The size of the destination window - public WeightsBuffer(int sourceSize, int destinationSize) - { - this.dataBuffer = Buffer2D.CreateClean(sourceSize, destinationSize); - this.Weights = new WeightsWindow[destinationSize]; - } - - /// - /// Gets the calculated values. - /// - public WeightsWindow[] Weights { get; } - - /// - /// Disposes instance releasing it's backing buffer. - /// - public void Dispose() - { - this.dataBuffer.Dispose(); - } - - /// - /// Slices a weights value at the given positions. - /// - /// The index in destination buffer - /// The local left index value - /// The local right index value - /// The weights - public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx) - { - return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1); - } - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs index 781f3a01c7..cb65559daa 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Adapted from /// /// The pixel format. - internal abstract partial class ResamplingWeightedProcessor : CloningImageProcessor + internal abstract class ResamplingWeightedProcessor : CloningImageProcessor where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index be49ec321b..e3fd36ce69 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -47,7 +47,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { - return Matrix3x2Extensions.CreateRotationDegrees(-this.Degrees, PointF.Empty); + Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(this.Degrees, PointF.Empty); + Matrix3x2.Invert(matrix, out matrix); + return matrix; } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 8da8b1e57b..eb9f068db0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -50,7 +50,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { - return Matrix3x2Extensions.CreateSkewDegrees(-this.DegreesX, -this.DegreesY, PointF.Empty); + Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(this.DegreesX, this.DegreesY, PointF.Empty); + Matrix3x2.Invert(matrix, out matrix); + return matrix; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index 62bdc4a1eb..45e9d4dd5e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -30,6 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public TransformProcessor(Matrix3x2 matrix, IResampler sampler) : base(sampler) { + Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs new file mode 100644 index 0000000000..0e91087f90 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Holds the values in an optimized contigous memory region. + /// + internal class WeightsBuffer : IDisposable + { + private readonly Buffer2D dataBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The size of the source window + /// The size of the destination window + public WeightsBuffer(int sourceSize, int destinationSize) + { + this.dataBuffer = Buffer2D.CreateClean(sourceSize, destinationSize); + this.Weights = new WeightsWindow[destinationSize]; + } + + /// + /// Gets the calculated values. + /// + public WeightsWindow[] Weights { get; } + + /// + /// Disposes instance releasing it's backing buffer. + /// + public void Dispose() + { + this.dataBuffer.Dispose(); + } + + /// + /// Slices a weights value at the given positions. + /// + /// The index in destination buffer + /// The local left index value + /// The local right index value + /// The weights + public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx) + { + return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs new file mode 100644 index 0000000000..d23ee5a060 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Points to a collection of of weights allocated in . + /// + internal struct WeightsWindow + { + /// + /// The local left index position + /// + public int Left; + + /// + /// The length of the weights window + /// + public int Length; + + /// + /// The index in the destination buffer + /// + private readonly int flatStartIndex; + + /// + /// The buffer containing the weights values. + /// + private readonly Buffer buffer; + + /// + /// Initializes a new instance of the struct. + /// + /// The destination index in the buffer + /// The local left index + /// The span + /// The length of the window + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal WeightsWindow(int index, int left, Buffer2D buffer, int length) + { + this.flatStartIndex = (index * buffer.Width) + left; + this.Left = left; + this.buffer = buffer; + this.Length = length; + } + + /// + /// Gets a reference to the first item of the window. + /// + /// The reference to the first item of the window + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref float GetStartReference() + { + return ref this.buffer[this.flatStartIndex]; + } + + /// + /// Gets the span representing the portion of the that this window covers + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length); + + /// + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// + /// The input span of vectors + /// The source row position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeWeightedRowSum(Span rowSpan, int sourceX) + { + ref float horizontalValues = ref this.GetStartReference(); + int left = this.Left; + ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float weight = Unsafe.Add(ref horizontalValues, i); + Vector4 v = Unsafe.Add(ref vecPtr, i); + result += v * weight; + } + + return result; + } + + /// + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// Applies to all input vectors. + /// + /// The input span of vectors + /// The source row position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeExpandedWeightedRowSum(Span rowSpan, int sourceX) + { + ref float horizontalValues = ref this.GetStartReference(); + int left = this.Left; + ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float weight = Unsafe.Add(ref horizontalValues, i); + Vector4 v = Unsafe.Add(ref vecPtr, i); + result += v.Expand() * weight; + } + + return result; + } + + /// + /// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x', + /// weighted by weight values, pointed by this instance. + /// + /// The buffer of input vectors in row first order + /// The row position + /// The source column position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeWeightedColumnSum(Buffer2D firstPassPixels, int x, int sourceY) + { + ref float verticalValues = ref this.GetStartReference(); + int left = this.Left; + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float yw = Unsafe.Add(ref verticalValues, i); + int index = left + i + sourceY; + result += firstPassPixels[x, index] * yw; + } + + return result; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs index 6dd6369802..98dbbadaba 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs @@ -40,11 +40,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { var proc = new ResizeProcessor(KnownResamplers.Bicubic, 200, 200); - ResamplingWeightedProcessor.WeightsBuffer weights = proc.PrecomputeWeights(200, 500); + WeightsBuffer weights = proc.PrecomputeWeights(200, 500); var bld = new StringBuilder(); - foreach (ResamplingWeightedProcessor.WeightsWindow window in weights.Weights) + foreach (WeightsWindow window in weights.Weights) { Span span = window.GetWindowSpan(); for (int i = 0; i < window.Length; i++) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 30cb626155..4562d7de73 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -17,8 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public static readonly TheoryData TransformValues = new TheoryData { - { 20, 10, 50 }, - { -20, -10, 50 } + { 20, 10, 45 }, + { -20, -10, 45 } }; public static readonly List ResamplerNames @@ -52,13 +52,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(-z); - - // TODO, how does scale work? 2 means half just now, + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(z); Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F)); - - image.Mutate(i => i.Transform(scale * rotate, sampler)); + image.Mutate(i => i.Transform(rotate * scale, sampler)); image.DebugSave(provider, string.Join("_", x, y, resamplerName)); } } From 13f73a963a7d7c76fbeeb571547a0a8a9d3748b6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Dec 2017 16:06:13 +1100 Subject: [PATCH 27/99] Span!! --- src/ImageSharp/Memory/Buffer{T}.cs | 2 +- .../Processors/Transforms/AffineProcessor.cs | 157 +++++++++--------- 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/src/ImageSharp/Memory/Buffer{T}.cs b/src/ImageSharp/Memory/Buffer{T}.cs index f5c9ed00a1..67af23426a 100644 --- a/src/ImageSharp/Memory/Buffer{T}.cs +++ b/src/ImageSharp/Memory/Buffer{T}.cs @@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Memory [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Buffer CreateClean(int count) { - Buffer buffer = new Buffer(count); + var buffer = new Buffer(count); buffer.Clear(); return buffer; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 767c6feed1..f2e74abd1b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -103,92 +103,95 @@ namespace SixLabors.ImageSharp.Processing.Processors int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); - Parallel.For( - 0, - height, - configuration.ParallelOptions, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - using (var yBuffer = new Buffer(yLength)) - using (var xBuffer = new Buffer(xLength)) - { - for (int x = 0; x < width; x++) + using (var yBuffer = new Buffer2D(yLength, height)) + using (var xBuffer = new Buffer2D(xLength, height)) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => { - // Use the single precision position to calculate correct bounding pixels - // otherwise we get rogue pixels outside of the bounds. - var point = Vector2.Transform(new Vector2(x, y), matrix); + Span destRow = destination.GetPixelRowSpan(y); + Span ySpan = yBuffer.GetRowSpan(y); + Span xSpan = xBuffer.GetRowSpan(y); - // Clamp sampling pixel radial extents to the source image edges - Vector2 maxXY = point + radius; - Vector2 minXY = point - radius; + for (int x = 0; x < width; x++) + { + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var point = Vector2.Transform(new Vector2(x, y), matrix); - var extents = new Vector4( - MathF.Ceiling(maxXY.X), - MathF.Ceiling(maxXY.Y), - MathF.Floor(minXY.X), - MathF.Floor(minXY.Y)); + // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; - int right = (int)extents.X; - int bottom = (int)extents.Y; - int left = (int)extents.Z; - int top = (int)extents.W; + var extents = new Vector4( + MathF.Ceiling(maxXY.X), + MathF.Ceiling(maxXY.Y), + MathF.Floor(minXY.X), + MathF.Floor(minXY.Y)); - extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + int right = (int)extents.X; + int bottom = (int)extents.Y; + int left = (int)extents.Z; + int top = (int)extents.W; - int maxX = (int)extents.X; - int maxY = (int)extents.Y; - int minX = (int)extents.Z; - int minY = (int)extents.W; + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); - if (minX == maxX || minY == maxY) - { - continue; - } + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; - // It appears these have to be calculated on-the-fly. - // Precalulating transformed weights would require prior knowledge of every transformed pixel location - // since they can be at sub-pixel positions. - // I've optimized where I can but am always open to suggestions. - // - // Create and normalize the y-weights - if (yScale > 1) - { - CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, yBuffer); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, yBuffer); - } + if (minX == maxX || minY == maxY) + { + continue; + } - // Create and normalize the x-weights - if (xScale > 1) - { - CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xBuffer); - } - else - { - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xBuffer); - } + // It appears these have to be calculated on-the-fly. + // Precalulating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions on both axis. + // I've optimized where I can but am always open to suggestions. + // + // Create and normalize the y-weights + if (yScale > 1) + { + CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); + } - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = yBuffer[yy]; + // Create and normalize the x-weights + if (xScale > 1) + { + CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); + } + else + { + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); + } - for (int xx = 0, i = minX; i <= maxX; i++, xx++) + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float xWeight = xBuffer[xx]; - sum += source[i, j].ToVector4() * xWeight * yWeight; + float yWeight = ySpan[yy]; + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = xSpan[xx]; + sum += source[i, j].ToVector4() * xWeight * yWeight; + } } - } - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(sum); - } - } - }); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(sum); + } + }); + } } /// @@ -206,7 +209,8 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Calculated the weights for the given point. + /// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. /// Additionally the weights are nomalized. /// /// The minimum sampling offset @@ -218,7 +222,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The transformed image scale relative to the source /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Buffer weights) + private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span weights) { float sum = 0; ref float weightsBaseRef = ref weights[0]; @@ -253,8 +257,7 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. - /// Additionally the weights are nomalized. + /// Calculated the weights for the given point. /// /// The minimum source bounds /// The maximum source bounds @@ -262,7 +265,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The sampler /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Buffer weights) + private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span weights) { ref float weightsBaseRef = ref weights[0]; for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) From 121f706b53346d041d5e01cbc03a36274d3493b1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Dec 2017 17:04:06 +1100 Subject: [PATCH 28/99] Add new transform draw test --- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 20 +++++++++++++++++++ .../Processing/Transforms/TransformTests.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 2e3a730fcc..6a55d8a561 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -10,6 +10,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests { using System; + using System.Numerics; public class DrawImageTest : FileTestBase { @@ -45,6 +46,25 @@ namespace SixLabors.ImageSharp.Tests } } + [Theory] + [WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Normal)] + public void ImageShouldDrawTransformedImage(TestImageProvider provider, PixelBlenderMode mode) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + using (Image blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F); + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F)); + + blend.Mutate(x => x.Transform(rotate * scale)); + + var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); + image.Mutate(x => x.DrawImage(blend, mode, .75F, new Size(blend.Width, blend.Height), position)); + image.DebugSave(provider, new[] { "Transformed" }); + } + } + [Theory] [WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32)] public void ImageShouldHandleNegativeLocation(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 4562d7de73..8dad6de412 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(z); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F)); + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); image.Mutate(i => i.Transform(rotate * scale, sampler)); image.DebugSave(provider, string.Join("_", x, y, resamplerName)); From 4f1b6dd325133c60577e591c2a3b7044abfcb9a6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 7 Dec 2017 12:12:40 +1100 Subject: [PATCH 29/99] Add comments --- .../Processing/Processors/Transforms/RotateProcessor.cs | 1 + .../Processing/Processors/Transforms/SkewProcessor.cs | 1 + .../Processing/Processors/Transforms/TransformProcessor.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index e3fd36ce69..b59918cea4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -47,6 +47,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { + // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(this.Degrees, PointF.Empty); Matrix3x2.Invert(matrix, out matrix); return matrix; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index eb9f068db0..321a6abe16 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -50,6 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { + // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(this.DegreesX, this.DegreesY, PointF.Empty); Matrix3x2.Invert(matrix, out matrix); return matrix; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index 45e9d4dd5e..6ed8c31dd4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -30,6 +30,8 @@ namespace SixLabors.ImageSharp.Processing.Processors public TransformProcessor(Matrix3x2 matrix, IResampler sampler) : base(sampler) { + + // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; } From 163f4d2e09d4032a01ac8d539703bf53d80f5cc0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 7 Dec 2017 12:33:13 +1100 Subject: [PATCH 30/99] Fix build --- .../Processing/Processors/Transforms/TransformProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index 6ed8c31dd4..e2dbed7655 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -30,7 +30,6 @@ namespace SixLabors.ImageSharp.Processing.Processors public TransformProcessor(Matrix3x2 matrix, IResampler sampler) : base(sampler) { - // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; From 6e690b27fabf8624644716e23944244cfa338a29 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Dec 2017 10:19:07 +1100 Subject: [PATCH 31/99] No need for Vector2 here. --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 74d705d53a..554de10bec 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -162,11 +162,10 @@ namespace SixLabors.ImageSharp float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); - var min = new Vector2(minX, minY); - var max = new Vector2(maxX, maxY); - Vector2 size = max - min; + float sizeX = maxX - minX; + float sizeY = maxY - minY; - return new Rectangle((int)MathF.Floor(minX), (int)MathF.Floor(minY), (int)MathF.Ceiling(size.X), (int)MathF.Ceiling(size.Y)); + return new Rectangle((int)MathF.Floor(minX), (int)MathF.Floor(minY), (int)MathF.Ceiling(sizeX), (int)MathF.Ceiling(sizeY)); } /// From 2890969a2d0f59bc522a470477bc29e03fe67d24 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 11 Dec 2017 02:38:17 +0100 Subject: [PATCH 32/99] better TransformTests --- .../Processing/Transforms/TransformTests.cs | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 8dad6de412..129fe1be3b 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +// ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Transforms { @@ -14,50 +15,69 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public class TransformTests : FileTestBase { - public static readonly TheoryData TransformValues - = new TheoryData + public static readonly TheoryData TransformValues + = new TheoryData { - { 20, 10, 45 }, - { -20, -10, 45 } + { 45, 1, 1, 20, 10 }, + { 45, 1, 1, -20, -10 }, + { 45, 1.5f, 1.5f, 0, 0 }, + { 0, 2f, 1f, 0, 0 }, + { 0, 1f, 2f, 0, 0 }, }; - public static readonly List ResamplerNames - = new List + public static readonly TheoryData ResamplerNames = + new TheoryData { nameof(KnownResamplers.Bicubic), - //nameof(KnownResamplers.Box), - //nameof(KnownResamplers.CatmullRom), - //nameof(KnownResamplers.Hermite), - //nameof(KnownResamplers.Lanczos2), - //nameof(KnownResamplers.Lanczos3), - //nameof(KnownResamplers.Lanczos5), - //nameof(KnownResamplers.Lanczos8), - //nameof(KnownResamplers.MitchellNetravali), - //nameof(KnownResamplers.NearestNeighbor), - //nameof(KnownResamplers.Robidoux), - //nameof(KnownResamplers.RobidouxSharp), - //nameof(KnownResamplers.Spline), - //nameof(KnownResamplers.Triangle), - //nameof(KnownResamplers.Welch), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(TransformValues), DefaultPixelType)] - public void ImageShouldTransformWithSampler(TestImageProvider provider, float x, float y, float z) + [WithTestPatternImages(nameof(TransformValues), 100, 50, DefaultPixelType)] + public void Transform_RotateScaleTranslate( + TestImageProvider provider, + float angleDeg, + float sx, float sy, + float tx, float ty) where TPixel : struct, IPixel { + using (Image image = provider.GetImage()) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); + Matrix3x2 translate = Matrix3x2Extensions.CreateTranslation(new PointF(tx, ty)); + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(sx, sy)); + + image.Mutate(i => i.Transform(rotate * scale * translate)); + image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); + } + } - foreach (string resamplerName in ResamplerNames) + [Theory] + [WithTestPatternImages(nameof(ResamplerNames), 100, 200, DefaultPixelType)] + public void Transform_WithSampler(TestImageProvider provider, string resamplerName) + where TPixel : struct, IPixel + { + IResampler sampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) { - IResampler sampler = GetResampler(resamplerName); - using (Image image = provider.GetImage()) - { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(z); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45); + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); - image.Mutate(i => i.Transform(rotate * scale, sampler)); - image.DebugSave(provider, string.Join("_", x, y, resamplerName)); - } + image.Mutate(i => i.Transform(rotate * scale, sampler)); + image.DebugSave(provider, resamplerName); } } From 0f98ca725b9d1be5080ca9cf2fd49b33d22c269d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 11 Dec 2017 16:07:32 +1100 Subject: [PATCH 33/99] Fix Lancsoz banding + lighter skew tests --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 6 ++--- .../Processors/Transforms/AffineProcessor.cs | 24 +++++++------------ .../Processors/Transforms/SkewTest.cs | 4 ++-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 554de10bec..a1c83415bf 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -162,10 +162,10 @@ namespace SixLabors.ImageSharp float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); - float sizeX = maxX - minX; - float sizeY = maxY - minY; + float sizeX = maxX - minX + .5F; + float sizeY = maxY - minY + .5F; - return new Rectangle((int)MathF.Floor(minX), (int)MathF.Floor(minY), (int)MathF.Ceiling(sizeX), (int)MathF.Ceiling(sizeY)); + return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY)); } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index f2e74abd1b..c9a4b2aeaf 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -127,10 +127,10 @@ namespace SixLabors.ImageSharp.Processing.Processors Vector2 minXY = point - radius; var extents = new Vector4( - MathF.Ceiling(maxXY.X), - MathF.Ceiling(maxXY.Y), - MathF.Floor(minXY.X), - MathF.Floor(minXY.Y)); + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F), + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F)); int right = (int)extents.X; int bottom = (int)extents.Y; @@ -154,23 +154,17 @@ namespace SixLabors.ImageSharp.Processing.Processors // since they can be at sub-pixel positions on both axis. // I've optimized where I can but am always open to suggestions. // - // Create and normalize the y-weights - if (yScale > 1) + // TODO: If we can somehow improve edge pixel handling that would be most beneficial. + // Currently the interpolated edge non-alpha components are altered which causes slight darkening of edges. + // Ideally the edge pixels should represent the nearest sample with altered alpha component only. + if (yScale > 1 && xScale > 1) { CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); - } - - // Create and normalize the x-weights - if (xScale > 1) - { CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); } else { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs index 8e57327be0..17bf3def26 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(SkewValues), DefaultPixelType)] + [WithTestPatternImages(nameof(SkewValues), 100, 50, DefaultPixelType)] public void ImageShouldSkew(TestImageProvider provider, float x, float y) where TPixel : struct, IPixel { @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(SkewValues), DefaultPixelType)] + [WithTestPatternImages(nameof(SkewValues), 100, 50, DefaultPixelType)] public void ImageShouldSkewWithSampler(TestImageProvider provider, float x, float y) where TPixel : struct, IPixel { From c8b1b77d09562fa3f59b5506638fe53addbdbf1c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 12 Dec 2017 12:10:15 +0100 Subject: [PATCH 34/99] failing test for edge artifacts, minimalistic RotateTests, better Rgba32.ToString() --- src/ImageSharp/PixelFormats/Rgb24.cs | 6 ++ src/ImageSharp/PixelFormats/Rgba32.cs | 2 +- .../Processors/Transforms/RotateTests.cs | 62 ++------------- .../Processing/Transforms/TransformTests.cs | 79 +++++++++++++++---- 4 files changed, 77 insertions(+), 72 deletions(-) diff --git a/src/ImageSharp/PixelFormats/Rgb24.cs b/src/ImageSharp/PixelFormats/Rgb24.cs index 5a12cff201..d867d9065e 100644 --- a/src/ImageSharp/PixelFormats/Rgb24.cs +++ b/src/ImageSharp/PixelFormats/Rgb24.cs @@ -126,5 +126,11 @@ namespace SixLabors.ImageSharp.PixelFormats dest.B = this.B; dest.A = 255; } + + /// + public override string ToString() + { + return $"({this.R},{this.G},{this.B})"; + } } } \ No newline at end of file diff --git a/src/ImageSharp/PixelFormats/Rgba32.cs b/src/ImageSharp/PixelFormats/Rgba32.cs index 51647fc1f9..83a35f1895 100644 --- a/src/ImageSharp/PixelFormats/Rgba32.cs +++ b/src/ImageSharp/PixelFormats/Rgba32.cs @@ -364,7 +364,7 @@ namespace SixLabors.ImageSharp /// A string representation of the packed vector. public override string ToString() { - return this.ToVector4().ToString(); + return $"({this.R},{this.G},{this.B},{this.A})"; } /// diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index ca29009bc7..b5220ea948 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs @@ -13,11 +13,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public class RotateTests : FileTestBase { - public static readonly TheoryData RotateFloatValues + public static readonly TheoryData RotateAngles = new TheoryData { - 170, - -170 + 50, -50, 170, -170 }; public static readonly TheoryData RotateEnumValues @@ -28,31 +27,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms RotateType.Rotate180, RotateType.Rotate270 }; - - public static readonly TheoryData ResamplerNames - = new TheoryData - { - nameof(KnownResamplers.Bicubic), - nameof(KnownResamplers.Box), - nameof(KnownResamplers.CatmullRom), - nameof(KnownResamplers.Hermite), - nameof(KnownResamplers.Lanczos2), - nameof(KnownResamplers.Lanczos3), - nameof(KnownResamplers.Lanczos5), - nameof(KnownResamplers.Lanczos8), - nameof(KnownResamplers.MitchellNetravali), - nameof(KnownResamplers.NearestNeighbor), - nameof(KnownResamplers.Robidoux), - nameof(KnownResamplers.RobidouxSharp), - nameof(KnownResamplers.Spline), - nameof(KnownResamplers.Triangle), - nameof(KnownResamplers.Welch), - }; - + [Theory] - [WithTestPatternImages(nameof(RotateFloatValues), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(RotateFloatValues), 50, 100, DefaultPixelType)] - public void Rotate(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)] + public void Rotate_WithAngle(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -61,22 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.DebugSave(provider, value); } } - - [Theory] - [WithTestPatternImages(nameof(ResamplerNames), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(ResamplerNames), 50, 100, DefaultPixelType)] - public void RotateWithSampler(TestImageProvider provider, string resamplerName) - where TPixel : struct, IPixel - { - IResampler resampler = GetResampler(resamplerName); - - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Rotate(50, resampler)); - image.DebugSave(provider, resamplerName); - } - } - + [Theory] [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)] @@ -89,17 +53,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms image.DebugSave(provider, value); } } - - private static IResampler GetResampler(string name) - { - PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); - - if (property == null) - { - throw new Exception("Invalid property name!"); - } - - return (IResampler)property.GetValue(null); - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 129fe1be3b..c4a9437d51 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -1,20 +1,19 @@ using System; -using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; +using Xunit; +using Xunit.Abstractions; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - using System.Numerics; - using System.Reflection; - - using SixLabors.ImageSharp.PixelFormats; - using SixLabors.ImageSharp.Processing; - using SixLabors.Primitives; - - using Xunit; - - public class TransformTests : FileTestBase + public class TransformTests { + private readonly ITestOutputHelper Output; + public static readonly TheoryData TransformValues = new TheoryData { @@ -45,8 +44,37 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms nameof(KnownResamplers.Welch), }; + public TransformTests(ITestOutputHelper output) + { + this.Output = output; + } + + /// + /// The output of an "all white" image should be "all white" or transparent, regardless of the transformation and the resampler. + /// + [Theory] + [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor))] + [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Triangle))] + [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic))] + [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos8))] + public void Transform_DoesNotCreateEdgeArtifacts(TestImageProvider provider, string resamplerName) + where TPixel : struct, IPixel + { + IResampler resampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) + { + // TODO: Modify this matrix if we change our origin-concept + var rotate = Matrix3x2.CreateRotation((float)Math.PI/4f); + + image.Mutate(c => c.Transform(rotate, resampler)); + image.DebugSave(provider, resamplerName); + + VerifyAllPixelsAreWhiteOrTransparent(image); + } + } + [Theory] - [WithTestPatternImages(nameof(TransformValues), 100, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] public void Transform_RotateScaleTranslate( TestImageProvider provider, float angleDeg, @@ -57,16 +85,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); - Matrix3x2 translate = Matrix3x2Extensions.CreateTranslation(new PointF(tx, ty)); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(sx, sy)); + var translate = Matrix3x2.CreateTranslation(tx, ty); + var scale = Matrix3x2.CreateScale(sx, sy); + Matrix3x2 m = rotate * scale * translate; + + this.Output.WriteLine(m.ToString()); - image.Mutate(i => i.Transform(rotate * scale * translate)); + image.Mutate(i => i.Transform(m)); image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); } } [Theory] - [WithTestPatternImages(nameof(ResamplerNames), 100, 200, DefaultPixelType)] + [WithTestPatternImages(nameof(ResamplerNames), 100, 200, PixelTypes.Rgba32)] public void Transform_WithSampler(TestImageProvider provider, string resamplerName) where TPixel : struct, IPixel { @@ -92,5 +123,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms return (IResampler)property.GetValue(null); } + + private static void VerifyAllPixelsAreWhiteOrTransparent(Image image) + where TPixel : struct, IPixel + { + TPixel[] data = new TPixel[image.Width * image.Height]; + image.Frames.RootFrame.SavePixelData(data); + var rgba = default(Rgba32); + var white = new Rgb24(255, 255, 255); + foreach (TPixel pixel in data) + { + pixel.ToRgba32(ref rgba); + if (rgba.A == 0) continue; + + Assert.Equal(white, rgba.Rgb); + } + } } } \ No newline at end of file From dcfa0a02c7707fc3f3da8829e5d2299ac0d1daf4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Dec 2017 00:06:10 +1100 Subject: [PATCH 35/99] Refactor transforms to allow freeform + rectangle --- .../Processors/Transforms/AffineProcessor.cs | 84 ++++++++++++------- .../Transforms/CenteredAffineProcessor.cs | 31 +++++++ .../Processors/Transforms/RotateProcessor.cs | 52 +++++++----- .../Processors/Transforms/SkewProcessor.cs | 14 +--- .../Transforms/TransformProcessor.cs | 18 ++-- .../Processing/Transforms/Transform.cs | 16 +++- 6 files changed, 141 insertions(+), 74 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index c9a4b2aeaf..b331201cc9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -23,32 +24,53 @@ namespace SixLabors.ImageSharp.Processing.Processors where TPixel : struct, IPixel { private Rectangle targetRectangle; - private Matrix3x2 transformMatrix; /// /// Initializes a new instance of the class. /// + /// The transform matrix /// The sampler to perform the transform operation. - protected AffineProcessor(IResampler sampler) + protected AffineProcessor(Matrix3x2 matrix, IResampler sampler) + : this(matrix, sampler, Rectangle.Empty) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + /// The rectangle to constrain the transformed image to. + protected AffineProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + { + // Tansforms are inverted else the output is the opposite of the expected. + Matrix3x2.Invert(matrix, out matrix); + this.TransformMatrix = matrix; + this.Sampler = sampler; + + this.targetRectangle = rectangle; } /// - /// Gets the sampler to perform interpolation of the transform operation. + /// Gets the matrix used to supply the affine transform /// - public IResampler Sampler { get; } + public Matrix3x2 TransformMatrix { get; } /// - /// Returns the processing matrix used for transforming the image. + /// Gets the sampler to perform interpolation of the transform operation. /// - /// The - protected abstract Matrix3x2 GetTransformMatrix(); + public IResampler Sampler { get; } /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - this.ResizeCanvas(sourceRectangle); + if (this.targetRectangle == Rectangle.Empty) + { + this.targetRectangle = Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix) + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; + } // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = @@ -66,7 +88,7 @@ namespace SixLabors.ImageSharp.Processing.Processors Rectangle sourceBounds = source.Bounds(); // Since could potentially be resizing the canvas we need to re-center the matrix - Matrix3x2 matrix = this.GetCenteredMatrix(source); + Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); if (this.Sampler is NearestNeighborResampler) { @@ -188,18 +210,37 @@ namespace SixLabors.ImageSharp.Processing.Processors } } + /// + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) + { + ExifProfile profile = destination.MetaData.ExifProfile; + if (profile == null) + { + return; + } + + if (profile.GetValue(ExifTag.PixelXDimension) != null) + { + profile.SetValue(ExifTag.PixelXDimension, destination.Width); + } + + if (profile.GetValue(ExifTag.PixelYDimension) != null) + { + profile.SetValue(ExifTag.PixelYDimension, destination.Height); + } + } + /// - /// Gets a transform matrix adjusted to center upon the target image bounds. + /// Gets a transform matrix adjusted for final processing based upon the target image bounds. /// - /// The source image. + /// The source image bounds. + /// The destination image bounds. /// /// The . /// - protected Matrix3x2 GetCenteredMatrix(ImageFrame source) + protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) { - var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.targetRectangle.Width * .5F, -this.targetRectangle.Height * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); - return translationToTargetCenter * this.transformMatrix * translateToSourceCenter; + return this.TransformMatrix; } /// @@ -269,19 +310,6 @@ namespace SixLabors.ImageSharp.Processing.Processors } } - /// - /// Creates a new target canvas to contain the results of the matrix transform. - /// - /// The source rectangle. - private void ResizeCanvas(Rectangle sourceRectangle) - { - this.transformMatrix = this.GetTransformMatrix(); - - this.targetRectangle = Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; - } - /// /// Calculates the sampling radius for the current sampler /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs new file mode 100644 index 0000000000..5f03f94e27 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + internal abstract class CenteredAffineProcessor : AffineProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + protected CenteredAffineProcessor(Matrix3x2 matrix, IResampler sampler) + : base(matrix, sampler) + { + } + + /// + protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) + { + var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); + return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index b59918cea4..7af1c68f12 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; @@ -16,15 +15,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : AffineProcessor + internal class RotateProcessor : CenteredAffineProcessor where TPixel : struct, IPixel { /// /// Initializes a new instance of the class. /// - /// The angle of rotation in degrees. - public RotateProcessor(float angle) - : this(angle, KnownResamplers.NearestNeighbor) + /// The angle of rotation in degrees. + public RotateProcessor(float degrees) + : this(degrees, KnownResamplers.NearestNeighbor) { } @@ -34,7 +33,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The angle of rotation in degrees. /// The sampler to perform the rotating operation. public RotateProcessor(float degrees, IResampler sampler) - : base(sampler) + : base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler) { this.Degrees = degrees; } @@ -44,15 +43,6 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public float Degrees { get; } - /// - protected override Matrix3x2 GetTransformMatrix() - { - // Tansforms are inverted else the output is the opposite of the expected. - Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(this.Degrees, PointF.Empty); - Matrix3x2.Invert(matrix, out matrix); - return matrix; - } - /// protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { @@ -73,7 +63,7 @@ namespace SixLabors.ImageSharp.Processing.Processors return; } - if (MathF.Abs(this.Degrees) < Constants.Epsilon) + if (MathF.Abs(WrapDegrees(this.Degrees)) < Constants.Epsilon) { // No need to do anything so return. return; @@ -81,11 +71,24 @@ namespace SixLabors.ImageSharp.Processing.Processors profile.RemoveValue(ExifTag.Orientation); - if (profile.GetValue(ExifTag.PixelXDimension) != null) + base.AfterImageApply(source, destination, sourceRectangle); + } + + /// + /// Wraps a given angle in degrees so that it falls withing the 0-360 degree range + /// + /// The angle of rotation in degrees. + /// The + private static float WrapDegrees(float degrees) + { + degrees = degrees % 360; + + if (degrees < 0) { - profile.SetValue(ExifTag.PixelXDimension, source.Width); - profile.SetValue(ExifTag.PixelYDimension, source.Height); + degrees += 360; } + + return degrees; } /// @@ -99,26 +102,29 @@ namespace SixLabors.ImageSharp.Processing.Processors /// private bool OptimizedApply(ImageFrame source, ImageFrame destination, Configuration configuration) { - if (MathF.Abs(this.Degrees) < Constants.Epsilon) + // Wrap the degrees to keep within 0-360 so we can apply optimizations when possible. + float degrees = WrapDegrees(this.Degrees); + + if (MathF.Abs(degrees) < Constants.Epsilon) { // The destination will be blank here so copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return true; } - if (MathF.Abs(this.Degrees - 90) < Constants.Epsilon) + if (MathF.Abs(degrees - 90) < Constants.Epsilon) { this.Rotate90(source, destination, configuration); return true; } - if (MathF.Abs(this.Degrees - 180) < Constants.Epsilon) + if (MathF.Abs(degrees - 180) < Constants.Epsilon) { this.Rotate180(source, destination, configuration); return true; } - if (MathF.Abs(this.Degrees - 270) < Constants.Epsilon) + if (MathF.Abs(degrees - 270) < Constants.Epsilon) { this.Rotate270(source, destination, configuration); return true; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 321a6abe16..07f082838c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -11,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : AffineProcessor + internal class SkewProcessor : CenteredAffineProcessor where TPixel : struct, IPixel { /// @@ -31,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The angle in degrees to perform the skew along the y-axis. /// The sampler to perform the skew operation. public SkewProcessor(float degreesX, float degreesY, IResampler sampler) - : base(sampler) + : base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler) { this.DegreesX = degreesX; this.DegreesY = degreesY; @@ -46,14 +45,5 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Gets the angle of rotation along the y-axis in degrees. /// public float DegreesY { get; } - - /// - protected override Matrix3x2 GetTransformMatrix() - { - // Tansforms are inverted else the output is the opposite of the expected. - Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(this.DegreesX, this.DegreesY, PointF.Empty); - Matrix3x2.Invert(matrix, out matrix); - return matrix; - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index e2dbed7655..577691cbb5 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors { @@ -28,22 +29,19 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The transformation matrix /// The sampler to perform the transform operation. public TransformProcessor(Matrix3x2 matrix, IResampler sampler) - : base(sampler) + : base(matrix, sampler) { - // Tansforms are inverted else the output is the opposite of the expected. - Matrix3x2.Invert(matrix, out matrix); - this.TransformMatrix = matrix; } /// - /// Gets the transform matrix + /// Initializes a new instance of the class. /// - public Matrix3x2 TransformMatrix { get; } - - /// - protected override Matrix3x2 GetTransformMatrix() + /// The transform matrix + /// The sampler to perform the transform operation. + /// The rectangle to constrain the transformed image to. + public TransformProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + : base(matrix, sampler, rectangle) { - return this.TransformMatrix; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index f3478e32d5..74f91fa701 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -5,6 +5,7 @@ using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; namespace SixLabors.ImageSharp { @@ -34,6 +35,19 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new TransformProcessor(matrix, sampler)); + => Transform(source, matrix, sampler, Rectangle.Empty); + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to skew. + /// The transformation matrix. + /// The to perform the resampling. + /// The rectangle to constrain the transformed image to. + /// The + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new TransformProcessor(matrix, sampler, rectangle)); } } \ No newline at end of file From 9be44f0c982e604cd0844b96e623accd140a2c84 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Dec 2017 01:14:58 +1100 Subject: [PATCH 36/99] Premultiply FTW! --- .../Processors/Transforms/AffineProcessor.cs | 6 ++++-- .../Processing/Transforms/TransformTests.cs | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index b331201cc9..6fdf061b86 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -199,12 +199,14 @@ namespace SixLabors.ImageSharp.Processing.Processors for (int xx = 0, i = minX; i <= maxX; i++, xx++) { float xWeight = xSpan[xx]; - sum += source[i, j].ToVector4() * xWeight * yWeight; + var vector = source[i, j].ToVector4(); + var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); + sum += mupltiplied * xWeight * yWeight; } } ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(sum); + dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); } }); } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index c4a9437d51..b2e183de79 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -17,14 +17,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public static readonly TheoryData TransformValues = new TheoryData { - { 45, 1, 1, 20, 10 }, - { 45, 1, 1, -20, -10 }, - { 45, 1.5f, 1.5f, 0, 0 }, + { 50, 1, 1, 20, 10 }, + { 50, 1, 1, -20, -10 }, + { 50, 1.5f, 1.5f, 0, 0 }, { 0, 2f, 1f, 0, 0 }, { 0, 1f, 2f, 0, 0 }, }; - public static readonly TheoryData ResamplerNames = + public static readonly TheoryData ResamplerNames = new TheoryData { nameof(KnownResamplers.Bicubic), @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { // TODO: Modify this matrix if we change our origin-concept - var rotate = Matrix3x2.CreateRotation((float)Math.PI/4f); + var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4f); image.Mutate(c => c.Transform(rotate, resampler)); image.DebugSave(provider, resamplerName); @@ -88,9 +88,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var translate = Matrix3x2.CreateTranslation(tx, ty); var scale = Matrix3x2.CreateScale(sx, sy); Matrix3x2 m = rotate * scale * translate; - + this.Output.WriteLine(m.ToString()); - + image.Mutate(i => i.Transform(m)); image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); } @@ -104,10 +104,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45); + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(50); Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); + var translate = Matrix3x2.CreateTranslation(75, 0); + - image.Mutate(i => i.Transform(rotate * scale, sampler)); + image.Mutate(i => i.Transform(rotate * scale * translate, sampler)); image.DebugSave(provider, resamplerName); } } From 9e13a378a619b7521999fc46323be5b465826bef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Dec 2017 10:22:25 +1100 Subject: [PATCH 37/99] Cleanup + update tests --- .../Processors/Transforms/AffineProcessor.cs | 8 +++---- .../Processors/Transforms/ResizeProcessor.cs | 23 ++++++++++++++++++- .../Processing/Transforms/TransformTests.cs | 6 ++--- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 6fdf061b86..ad8221b88b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -175,10 +175,6 @@ namespace SixLabors.ImageSharp.Processing.Processors // Precalulating transformed weights would require prior knowledge of every transformed pixel location // since they can be at sub-pixel positions on both axis. // I've optimized where I can but am always open to suggestions. - // - // TODO: If we can somehow improve edge pixel handling that would be most beneficial. - // Currently the interpolated edge non-alpha components are altered which causes slight darkening of edges. - // Ideally the edge pixels should represent the nearest sample with altered alpha component only. if (yScale > 1 && xScale > 1) { CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); @@ -200,12 +196,16 @@ namespace SixLabors.ImageSharp.Processing.Processors { float xWeight = xSpan[xx]; var vector = source[i, j].ToVector4(); + + // Values are first premultiplied to prevent darkening of edge pixels var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); sum += mupltiplied * xWeight * yWeight; } } ref TPixel dest = ref destRow[x]; + + // Reverse the premultiplication dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); } }); diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index ecfcc7dd20..0d8d0d9117 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -62,7 +63,7 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - protected override unsafe void OnApply(ImageFrame source, ImageFrame cloned, Rectangle sourceRectangle, Configuration configuration) + protected override void OnApply(ImageFrame source, ImageFrame cloned, Rectangle sourceRectangle, Configuration configuration) { // Jump out, we'll deal with that later. if (source.Width == cloned.Width && source.Height == cloned.Height && sourceRectangle == this.ResizeRectangle) @@ -190,5 +191,25 @@ namespace SixLabors.ImageSharp.Processing.Processors }); } } + + /// + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) + { + ExifProfile profile = destination.MetaData.ExifProfile; + if (profile == null) + { + return; + } + + if (profile.GetValue(ExifTag.PixelXDimension) != null) + { + profile.SetValue(ExifTag.PixelXDimension, destination.Width); + } + + if (profile.GetValue(ExifTag.PixelYDimension) != null) + { + profile.SetValue(ExifTag.PixelYDimension, destination.Height); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index b2e183de79..54a3af36fd 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -63,10 +63,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler resampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - // TODO: Modify this matrix if we change our origin-concept - var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4f); + var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F)); + var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F); - image.Mutate(c => c.Transform(rotate, resampler)); + image.Mutate(c => c.Transform(rotate * translate, resampler)); image.DebugSave(provider, resamplerName); VerifyAllPixelsAreWhiteOrTransparent(image); From 2391577c74768a3c8b3a0951ac9529070f5330f0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 01:13:20 +1100 Subject: [PATCH 38/99] Refactor filters to match CSS & SVG methods --- .../{Effects => ColorMatrix}/Brightness.cs | 16 +- .../{Effects => ColorMatrix}/Contrast.cs | 19 +- .../Processing/ColorMatrix/Grayscale.cs | 72 ++++- src/ImageSharp/Processing/ColorMatrix/Hue.cs | 6 +- .../ColorMatrix/Matrix4x4Extensions.cs | 257 ++++++++++++++++++ .../Alpha.cs => ColorMatrix/Opacity.cs} | 14 +- .../{Saturation.cs => Saturate.cs} | 22 +- .../Processing/ColorMatrix/Sepia.cs | 31 ++- src/ImageSharp/Processing/Effects/Invert.cs | 4 +- .../Binarization/BinaryThresholdProcessor.cs | 2 +- .../ErrorDiffusionDitherProcessor.cs | 2 +- .../Binarization/OrderedDitherProcessor.cs | 2 +- .../ColorMatrix/BrightnessProcessor.cs | 34 +++ .../ColorMatrix/ContrastProcessor.cs | 34 +++ .../Processors/ColorMatrix/FilterProcessor.cs | 62 +++++ .../ColorMatrix/GrayscaleBt601Processor.cs | 34 ++- .../ColorMatrix/GrayscaleBt709Processor.cs | 34 ++- .../Processors/ColorMatrix/HueProcessor.cs | 68 +---- .../Processors/ColorMatrix/InvertProcessor.cs | 30 ++ .../ColorMatrix/OpacityProcessor.cs | 30 ++ .../ColorMatrix/SaturateProcessor.cs | 34 +++ .../ColorMatrix/SaturationProcessor.cs | 66 ----- .../Processors/ColorMatrix/SepiaProcessor.cs | 35 +-- .../EdgeDetection/EdgeDetector2DProcessor.cs | 2 +- .../EdgeDetectorCompassProcessor.cs | 2 +- .../EdgeDetection/EdgeDetectorProcessor.cs | 2 +- .../Processors/Effects/AlphaProcessor.cs | 81 ------ .../Processors/Effects/BrightnessProcessor.cs | 87 ------ .../Processors/Effects/ContrastProcessor.cs | 89 ------ .../Processors/Effects/InvertProcessor.cs | 66 ----- .../Formats/GeneralFormatTests.cs | 2 + .../BrightnessTest.cs | 12 +- .../{Effects => ColorMatrix}/ContrastTest.cs | 14 +- .../Processing/ColorMatrix/HueTest.cs | 4 +- .../{Effects => ColorMatrix}/InvertTest.cs | 0 .../Processing/ColorMatrix/OpacityTest.cs | 29 ++ .../{SaturationTest.cs => SaturateTest.cs} | 12 +- .../Processing/Effects/AlphaTest.cs | 31 --- .../BrightnessTest.cs | 14 +- .../{Effects => ColorMatrix}/ContrastTest.cs | 14 +- .../{Effects => ColorMatrix}/InvertTest.cs | 2 +- .../OpacityTest.cs} | 8 +- .../{SaturationTest.cs => SaturateTest.cs} | 20 +- .../Tests/ReferenceCodecTests.cs | 6 +- 44 files changed, 764 insertions(+), 641 deletions(-) rename src/ImageSharp/Processing/{Effects => ColorMatrix}/Brightness.cs (62%) rename src/ImageSharp/Processing/{Effects => ColorMatrix}/Contrast.cs (61%) create mode 100644 src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs rename src/ImageSharp/Processing/{Effects/Alpha.cs => ColorMatrix/Opacity.cs} (67%) rename src/ImageSharp/Processing/ColorMatrix/{Saturation.cs => Saturate.cs} (54%) create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/SaturationProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Effects/AlphaProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Effects/BrightnessProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Effects/ContrastProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Effects/InvertProcessor.cs rename tests/ImageSharp.Tests/Processing/{Effects => ColorMatrix}/BrightnessTest.cs (59%) rename tests/ImageSharp.Tests/Processing/{Effects => ColorMatrix}/ContrastTest.cs (55%) rename tests/ImageSharp.Tests/Processing/{Effects => ColorMatrix}/InvertTest.cs (100%) create mode 100644 tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs rename tests/ImageSharp.Tests/Processing/ColorMatrix/{SaturationTest.cs => SaturateTest.cs} (58%) delete mode 100644 tests/ImageSharp.Tests/Processing/Effects/AlphaTest.cs rename tests/ImageSharp.Tests/Processing/Processors/{Effects => ColorMatrix}/BrightnessTest.cs (83%) rename tests/ImageSharp.Tests/Processing/Processors/{Effects => ColorMatrix}/ContrastTest.cs (83%) rename tests/ImageSharp.Tests/Processing/Processors/{Effects => ColorMatrix}/InvertTest.cs (96%) rename tests/ImageSharp.Tests/Processing/Processors/{Effects/AlphaTest.cs => ColorMatrix/OpacityTest.cs} (87%) rename tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/{SaturationTest.cs => SaturateTest.cs} (74%) diff --git a/src/ImageSharp/Processing/Effects/Brightness.cs b/src/ImageSharp/Processing/ColorMatrix/Brightness.cs similarity index 62% rename from src/ImageSharp/Processing/Effects/Brightness.cs rename to src/ImageSharp/Processing/ColorMatrix/Brightness.cs index 5c76287858..34b5347841 100644 --- a/src/ImageSharp/Processing/Effects/Brightness.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Brightness.cs @@ -16,25 +16,33 @@ namespace SixLabors.ImageSharp /// /// Alters the brightness component of the image. /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// /// The pixel format. /// The image this method extends. - /// The new brightness of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// The . - public static IImageProcessingContext Brightness(this IImageProcessingContext source, int amount) + public static IImageProcessingContext Brightness(this IImageProcessingContext source, float amount) where TPixel : struct, IPixel => source.ApplyProcessor(new BrightnessProcessor(amount)); /// /// Alters the brightness component of the image. /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// /// The pixel format. /// The image this method extends. - /// The new brightness of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext Brightness(this IImageProcessingContext source, int amount, Rectangle rectangle) + public static IImageProcessingContext Brightness(this IImageProcessingContext source, float amount, Rectangle rectangle) where TPixel : struct, IPixel => source.ApplyProcessor(new BrightnessProcessor(amount), rectangle); } diff --git a/src/ImageSharp/Processing/Effects/Contrast.cs b/src/ImageSharp/Processing/ColorMatrix/Contrast.cs similarity index 61% rename from src/ImageSharp/Processing/Effects/Contrast.cs rename to src/ImageSharp/Processing/ColorMatrix/Contrast.cs index 562ab54dff..e0f388e287 100644 --- a/src/ImageSharp/Processing/Effects/Contrast.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Contrast.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; @@ -16,26 +15,34 @@ namespace SixLabors.ImageSharp /// /// Alters the contrast component of the image. /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// /// The pixel format. /// The image this method extends. - /// The new contrast of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// The . - public static IImageProcessingContext Contrast(this IImageProcessingContext source, int amount) + public static IImageProcessingContext Contrast(this IImageProcessingContext source, float amount) where TPixel : struct, IPixel => source.ApplyProcessor(new ContrastProcessor(amount)); /// /// Alters the contrast component of the image. /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// /// The pixel format. /// The image this method extends. - /// The new contrast of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext Contrast(this IImageProcessingContext source, int amount, Rectangle rectangle) + public static IImageProcessingContext Contrast(this IImageProcessingContext source, float amount, Rectangle rectangle) where TPixel : struct, IPixel => source.ApplyProcessor(new ContrastProcessor(amount), rectangle); } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs b/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs index bcf48d3e2c..4fa80a183f 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs @@ -21,12 +21,21 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Grayscale(this IImageProcessingContext source) where TPixel : struct, IPixel - { - return Grayscale(source, GrayscaleMode.Bt709); - } + => Grayscale(source, GrayscaleMode.Bt709); /// - /// Applies Grayscale toning to the image. + /// Applies Grayscale toning to the image using the given amount. + /// + /// The pixel format. + /// The image this method extends. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, float amount) + where TPixel : struct, IPixel + => Grayscale(source, GrayscaleMode.Bt709, amount); + + /// + /// Applies grayscale toning to the image with the given . /// /// The pixel format. /// The image this method extends. @@ -34,10 +43,22 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode) where TPixel : struct, IPixel + => Grayscale(source, mode, 1F); + + /// + /// Applies grayscale toning to the image with the given using the given amount. + /// + /// The pixel format. + /// The image this method extends. + /// The formula to apply to perform the operation. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, float amount) + where TPixel : struct, IPixel { IImageProcessor processor = mode == GrayscaleMode.Bt709 - ? (IImageProcessor)new GrayscaleBt709Processor() - : new GrayscaleBt601Processor(); + ? (IImageProcessor)new GrayscaleBt709Processor(amount) + : new GrayscaleBt601Processor(1F); source.ApplyProcessor(processor); return source; @@ -54,9 +75,21 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Grayscale(this IImageProcessingContext source, Rectangle rectangle) where TPixel : struct, IPixel - { - return Grayscale(source, GrayscaleMode.Bt709, rectangle); - } + => Grayscale(source, 1F, rectangle); + + /// + /// Applies Grayscale toning to the image using the given amount. + /// + /// The pixel format. + /// The image this method extends. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, float amount, Rectangle rectangle) + where TPixel : struct, IPixel + => Grayscale(source, GrayscaleMode.Bt709, amount, rectangle); /// /// Applies Grayscale toning to the image. @@ -70,13 +103,28 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, Rectangle rectangle) where TPixel : struct, IPixel + => Grayscale(source, GrayscaleMode.Bt709, 1F, rectangle); + + /// + /// Applies Grayscale toning to the image using the given amount. + /// + /// The pixel format. + /// The image this method extends. + /// The formula to apply to perform the operation. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, float amount, Rectangle rectangle) + where TPixel : struct, IPixel { IImageProcessor processor = mode == GrayscaleMode.Bt709 - ? (IImageProcessor)new GrayscaleBt709Processor() - : new GrayscaleBt601Processor(); + ? (IImageProcessor)new GrayscaleBt709Processor(amount) + : new GrayscaleBt601Processor(amount); source.ApplyProcessor(processor, rectangle); return source; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Hue.cs b/src/ImageSharp/Processing/ColorMatrix/Hue.cs index bfc931977d..76af10c369 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Hue.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Hue.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp /// /// The pixel format. /// The image this method extends. - /// The angle in degrees to adjust the image. + /// The rotation angle in degrees to adjust the hue. /// The . public static IImageProcessingContext Hue(this IImageProcessingContext source, float degrees) where TPixel : struct, IPixel @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp /// /// The pixel format. /// The image this method extends. - /// The angle in degrees to adjust the image. + /// The rotation angle in degrees to adjust the hue. /// /// The structure that specifies the portion of the image object to alter. /// @@ -45,4 +45,4 @@ namespace SixLabors.ImageSharp return source; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs b/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs new file mode 100644 index 0000000000..21c7f05e59 --- /dev/null +++ b/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Provides extensions methods for the struct + /// + // ReSharper disable once InconsistentNaming + public static class Matrix4x4Extensions + { + /// + /// Create a brightness filter matrix using the given amount. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static Matrix4x4 CreateBrightnessFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = amount, + M22 = amount, + M33 = amount, + M44 = 1 + }; + } + + /// + /// Create a contrast filter matrix using the given amount. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static Matrix4x4 CreateContrastFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + float contrast = (-.5F * amount) + .5F; + + return new Matrix4x4 + { + M11 = amount, + M22 = amount, + M33 = amount, + M41 = contrast, + M42 = contrast, + M43 = contrast, + M44 = 1 + }; + } + + /// + /// Create a greyscale filter matrix using the given amount using the formula as specified by ITU-R Recommendation BT.601. + /// + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static Matrix4x4 CreateGrayscaleBt601Filter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + amount = 1F - amount; + + // https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = .299F + (.701F * amount), + M12 = .299F - (.299F * amount), + M13 = .299F - (.299F * amount), + M21 = .587F - (.587F * amount), + M22 = .587F + (.413F * amount), + M23 = .587F - (.587F * amount), + M31 = .114F - (.114F * amount), + M32 = .114F - (.114F * amount), + M33 = .114F + (.886F * amount), + M44 = 1 + }; + } + + /// + /// Create a greyscale filter matrix using the given amount using the formula as specified by ITU-R Recommendation BT.709. + /// + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static Matrix4x4 CreateGrayscaleBt709Filter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + amount = 1F - amount; + + // https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = .2126F + (.7874F * amount), + M12 = .2126F - (.2126F * amount), + M13 = .2126F - (.2126F * amount), + M21 = .7152F - (.7152F * amount), + M22 = .7152F + (.2848F * amount), + M23 = .7152F - (.7152F * amount), + M31 = .0722F - (.0722F * amount), + M32 = .0722F - (.0722F * amount), + M33 = .0722F + (.9278F * amount), + M44 = 1 + }; + } + + /// + /// Create a hue filter matrix using the given angle in degrees. + /// + /// The angle of rotation in degrees. + /// The + public static Matrix4x4 CreateHueFilter(float degrees) + { + // Wrap the angle round at 360. + degrees = degrees % 360; + + // Make sure it's not negative. + while (degrees < 0) + { + degrees += 360; + } + + float radian = MathFExtensions.DegreeToRadian(degrees); + float cosRadian = MathF.Cos(radian); + float sinRadian = MathF.Sin(radian); + + // The matrix is set up to preserve the luminance of the image. + // See http://graficaobscura.com/matrix/index.html + // Number are taken from https://msdn.microsoft.com/en-us/library/jj192162(v=vs.85).aspx + return new Matrix4x4 + { + M11 = .213F + (cosRadian * .787F) - (sinRadian * .213F), + M12 = .213F - (cosRadian * .213F) - (sinRadian * 0.143F), + M13 = .213F - (cosRadian * .213F) - (sinRadian * .787F), + M21 = .715F - (cosRadian * .715F) - (sinRadian * .715F), + M22 = .715F + (cosRadian * .285F) + (sinRadian * 0.140F), + M23 = .715F - (cosRadian * .715F) + (sinRadian * .715F), + M31 = .072F - (cosRadian * .072F) + (sinRadian * .928F), + M32 = .072F - (cosRadian * .072F) - (sinRadian * 0.283F), + M33 = .072F + (cosRadian * .928F) + (sinRadian * .072F), + M44 = 1 + }; + } + + /// + /// Create an invert filter matrix using the given amount. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static Matrix4x4 CreateInvertFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + float invert = 1F - (2F * amount); + + return new Matrix4x4 + { + M11 = invert, + M22 = invert, + M33 = invert, + M41 = amount, + M42 = amount, + M43 = amount, + M44 = 1 + }; + } + + /// + /// Create an opacity filter matrix using the given amount. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static Matrix4x4 CreateOpacityFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = 1, + M22 = 1, + M33 = 1, + M44 = amount + }; + } + + /// + /// Create a saturation filter matrix using the given amount. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + /// The + public static Matrix4x4 CreateSaturateFilter(float amount) + { + Guard.MustBeGreaterThanOrEqualTo(amount, 0, nameof(amount)); + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = .213F + (.787F * amount), + M12 = .213F - (.213F * amount), + M13 = .213F - (.213F * amount), + M21 = .715F - (.715F * amount), + M22 = .715F + (.285F * amount), + M23 = .715F - (.715F * amount), + M31 = 1F - ((.213F + (.787F * amount)) + (.715F - (.715F * amount))), + M32 = 1F - ((.213F - (.213F * amount)) + (.715F + (.285F * amount))), + M33 = 1F - ((.213F - (.213F * amount)) + (.715F - (.715F * amount))), + M44 = 1 + }; + } + + /// + /// Create a sepia filter matrix using the given amount. + /// The formula used matches the svg specification. + /// + /// The proportion of the conversion. Must be between 0 and 1. + /// The + public static Matrix4x4 CreateSepiaFilter(float amount) + { + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + amount = 1F - amount; + + // See https://cs.chromium.org/chromium/src/cc/paint/render_surface_filters.cc + return new Matrix4x4 + { + M11 = .393F + (.607F * amount), + M12 = .349F - (.349F * amount), + M13 = .272F - (.272F * amount), + M21 = .769F - (.769F * amount), + M22 = .686F + (.314F * amount), + M23 = .534F - (.534F * amount), + M31 = .189F - (.189F * amount), + M32 = .168F - (.168F * amount), + M33 = .131F + (.869F * amount), + M44 = 1 + }; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Effects/Alpha.cs b/src/ImageSharp/Processing/ColorMatrix/Opacity.cs similarity index 67% rename from src/ImageSharp/Processing/Effects/Alpha.cs rename to src/ImageSharp/Processing/ColorMatrix/Opacity.cs index 2fac64e1cf..b310b4b915 100644 --- a/src/ImageSharp/Processing/Effects/Alpha.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Opacity.cs @@ -18,24 +18,24 @@ namespace SixLabors.ImageSharp /// /// The pixel format. /// The image this method extends. - /// The new opacity of the image. Must be between 0 and 1. + /// The proportion of the conversion. Must be between 0 and 1. /// The . - public static IImageProcessingContext Alpha(this IImageProcessingContext source, float percent) + public static IImageProcessingContext Opacity(this IImageProcessingContext source, float amount) where TPixel : struct, IPixel - => source.ApplyProcessor(new AlphaProcessor(percent)); + => source.ApplyProcessor(new OpacityProcessor(amount)); /// /// Alters the alpha component of the image. /// /// The pixel format. /// The image this method extends. - /// The new opacity of the image. Must be between 0 and 1. + /// The proportion of the conversion. Must be between 0 and 1. /// /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext Alpha(this IImageProcessingContext source, float percent, Rectangle rectangle) + public static IImageProcessingContext Opacity(this IImageProcessingContext source, float amount, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new AlphaProcessor(percent), rectangle); + => source.ApplyProcessor(new OpacityProcessor(amount), rectangle); } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Saturation.cs b/src/ImageSharp/Processing/ColorMatrix/Saturate.cs similarity index 54% rename from src/ImageSharp/Processing/ColorMatrix/Saturation.cs rename to src/ImageSharp/Processing/ColorMatrix/Saturate.cs index 26ca5ec204..c7dd395aa3 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Saturation.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Saturate.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; @@ -17,31 +15,39 @@ namespace SixLabors.ImageSharp /// /// Alters the saturation component of the image. /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// /// The pixel format. /// The image this method extends. - /// The new saturation of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// The . - public static IImageProcessingContext Saturation(this IImageProcessingContext source, int amount) + public static IImageProcessingContext Saturate(this IImageProcessingContext source, float amount) where TPixel : struct, IPixel { - source.ApplyProcessor(new SaturationProcessor(amount)); + source.ApplyProcessor(new SaturateProcessor(amount)); return source; } /// /// Alters the saturation component of the image. /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// /// The pixel format. /// The image this method extends. - /// The new saturation of the image. Must be between -100 and 100. + /// The proportion of the conversion. Must be greater than or equal to 0. /// /// The structure that specifies the portion of the image object to alter. /// /// The . - public static IImageProcessingContext Saturation(this IImageProcessingContext source, int amount, Rectangle rectangle) + public static IImageProcessingContext Saturate(this IImageProcessingContext source, float amount, Rectangle rectangle) where TPixel : struct, IPixel { - source.ApplyProcessor(new SaturationProcessor(amount), rectangle); + source.ApplyProcessor(new SaturateProcessor(amount), rectangle); return source; } } diff --git a/src/ImageSharp/Processing/ColorMatrix/Sepia.cs b/src/ImageSharp/Processing/ColorMatrix/Sepia.cs index d1116fac8b..0d686f4dba 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Sepia.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Sepia.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; @@ -22,7 +20,18 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Sepia(this IImageProcessingContext source) where TPixel : struct, IPixel - => source.ApplyProcessor(new SepiaProcessor()); + => Sepia(source, 1F); + + /// + /// Applies sepia toning to the image using the given amount. + /// + /// The pixel format. + /// The image this method extends. + /// The proportion of the conversion. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source, float amount) + where TPixel : struct, IPixel + => source.ApplyProcessor(new SepiaProcessor(amount)); /// /// Applies sepia toning to the image. @@ -35,6 +44,20 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Sepia(this IImageProcessingContext source, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new SepiaProcessor(), rectangle); + => Sepia(source, 1F, rectangle); + + /// + /// Applies sepia toning to the image. + /// + /// The pixel format. + /// The image this method extends. + /// The proportion of the conversion. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Sepia(this IImageProcessingContext source, float amount, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new SepiaProcessor(amount), rectangle); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Effects/Invert.cs b/src/ImageSharp/Processing/Effects/Invert.cs index 9c0a7d3772..7dd9ed3dd7 100644 --- a/src/ImageSharp/Processing/Effects/Invert.cs +++ b/src/ImageSharp/Processing/Effects/Invert.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Invert(this IImageProcessingContext source) where TPixel : struct, IPixel - => source.ApplyProcessor(new InvertProcessor()); + => source.ApplyProcessor(new InvertProcessor(1F)); /// /// Inverts the colors of the image. @@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Invert(this IImageProcessingContext source, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new InvertProcessor(), rectangle); + => source.ApplyProcessor(new InvertProcessor(1F), rectangle); } } diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs index d736b91ee6..434ed02698 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } /// diff --git a/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs index 8907770e15..01cba15c4b 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } /// diff --git a/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs index a2fd17c94b..a37d12f18c 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } /// diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs new file mode 100644 index 0000000000..051a74ba25 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Applies a brightness filter matrix using the given amount. + /// + /// The pixel format. + internal class BrightnessProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing brighter results. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public BrightnessProcessor(float amount) + : base(Matrix4x4Extensions.CreateBrightnessFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs new file mode 100644 index 0000000000..a46d0c1dee --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Applies a contrast filter matrix using the given amount. + /// + /// The pixel format. + internal class ContrastProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 will create an image that is completely gray. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of an amount over 1 are allowed, providing results with more contrast. + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public ContrastProcessor(float amount) + : base(Matrix4x4Extensions.CreateContrastFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs new file mode 100644 index 0000000000..30fe8c6b6f --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides methods that accept a matrix to apply freeform filters to images. + /// + /// The pixel format. + internal class FilterProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The matrix used to apply the image filter + public FilterProcessor(Matrix4x4 matrix) + { + this.Matrix = matrix; + } + + /// + /// Gets the used to apply the image filter. + /// + public Matrix4x4 Matrix { get; } + + /// + protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + Matrix4x4 matrix = this.Matrix; + + Parallel.For( + startY, + endY, + configuration.ParallelOptions, + y => + { + Span row = source.GetPixelRowSpan(y); + + for (int x = startX; x < endX; x++) + { + ref TPixel pixel = ref row[x]; + var vector = Vector4.Transform(pixel.ToVector4(), matrix); + pixel.PackFromVector4(vector); + } + }); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs index 35dfe41a82..fde0665d30 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs @@ -1,32 +1,30 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Converts the colors of the image to Grayscale applying the formula as specified by ITU-R Recommendation BT.601 - /// . + /// Applies a greyscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.601 /// /// The pixel format. - internal class GrayscaleBt601Processor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class GrayscaleBt601Processor : FilterProcessor + where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public GrayscaleBt601Processor(float amount) + : base(Matrix4x4Extensions.CreateGrayscaleBt601Filter(amount)) { - M11 = .299F, - M12 = .299F, - M13 = .299F, - M21 = .587F, - M22 = .587F, - M23 = .587F, - M31 = .114F, - M32 = .114F, - M33 = .114F, - M44 = 1 - }; + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs index 6bb460ee67..92195a30da 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs @@ -1,32 +1,30 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Converts the colors of the image to Grayscale applying the formula as specified by ITU-R Recommendation BT.709 - /// . + /// Applies a greyscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.709 /// /// The pixel format. - internal class GrayscaleBt709Processor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class GrayscaleBt709Processor : FilterProcessor + where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public GrayscaleBt709Processor(float amount) + : base(Matrix4x4Extensions.CreateGrayscaleBt709Filter(amount)) { - M11 = .2126F, - M12 = .2126F, - M13 = .2126F, - M21 = .7152F, - M22 = .7152F, - M23 = .7152F, - M31 = .0722F, - M32 = .0722F, - M33 = .0722F, - M44 = 1 - }; + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs index adfdb6a788..edb61855e9 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs @@ -1,77 +1,29 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// An to change the hue of an . + /// Applies a hue filter matrix using the given angle of rotation in degrees /// - /// The pixel format. - internal class HueProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class HueProcessor : FilterProcessor + where TPixel : struct, IPixel { /// /// Initializes a new instance of the class. /// - /// The new brightness of the image. Must be between -100 and 100. - public HueProcessor(float angle) + /// The angle of rotation in degrees + public HueProcessor(float degrees) + : base(Matrix4x4Extensions.CreateHueFilter(degrees)) { - // Wrap the angle round at 360. - angle = angle % 360; - - // Make sure it's not negative. - while (angle < 0) - { - angle += 360; - } - - this.Angle = angle; - - float radians = MathFExtensions.DegreeToRadian(angle); - float cosradians = MathF.Cos(radians); - float sinradians = MathF.Sin(radians); - - float lumR = .213F; - float lumG = .715F; - float lumB = .072F; - - float oneMinusLumR = 1 - lumR; - float oneMinusLumG = 1 - lumG; - float oneMinusLumB = 1 - lumB; - - // The matrix is set up to preserve the luminance of the image. - // See http://graficaobscura.com/matrix/index.html - // Number are taken from https://msdn.microsoft.com/en-us/library/jj192162(v=vs.85).aspx - var matrix4X4 = new Matrix4x4 - { - M11 = lumR + (cosradians * oneMinusLumR) - (sinradians * lumR), - M12 = lumR - (cosradians * lumR) - (sinradians * 0.143F), - M13 = lumR - (cosradians * lumR) - (sinradians * oneMinusLumR), - M21 = lumG - (cosradians * lumG) - (sinradians * lumG), - M22 = lumG + (cosradians * oneMinusLumG) + (sinradians * 0.140F), - M23 = lumG - (cosradians * lumG) + (sinradians * lumG), - M31 = lumB - (cosradians * lumB) + (sinradians * oneMinusLumB), - M32 = lumB - (cosradians * lumB) - (sinradians * 0.283F), - M33 = lumB + (cosradians * oneMinusLumB) + (sinradians * lumB), - M44 = 1 - }; - - this.Matrix = matrix4X4; + this.Degrees = degrees; } /// - /// Gets the rotation value. + /// Gets the angle of rotation in degrees /// - public float Angle { get; } - - /// - public override Matrix4x4 Matrix { get; } - - /// - public override bool Compand => false; + public float Degrees { get; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs new file mode 100644 index 0000000000..1a78c22b51 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Applies a filter matrix that inverts the colors of an image + /// + /// The pixel format. + internal class InvertProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public InvertProcessor(float amount) + : base(Matrix4x4Extensions.CreateInvertFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs new file mode 100644 index 0000000000..7247cec409 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Applies an opacity filter matrix using the given amount. + /// + /// The pixel format. + internal class OpacityProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public OpacityProcessor(float amount) + : base(Matrix4x4Extensions.CreateOpacityFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs new file mode 100644 index 0000000000..0a25161a88 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Applies a saturation filter matrix using the given amount. + /// + /// The pixel format. + internal class SaturateProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + /// Other values are linear multipliers on the effect. Values of amount over 1 are allowed, providing super-saturated results + /// + /// The proportion of the conversion. Must be greater than or equal to 0. + public SaturateProcessor(float amount) + : base(Matrix4x4Extensions.CreateSaturateFilter(amount)) + { + this.Amount = amount; + } + + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/SaturationProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/SaturationProcessor.cs deleted file mode 100644 index 1f01bc85dc..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/SaturationProcessor.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An to change the saturation of an . - /// - /// The pixel format. - internal class SaturationProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The new saturation of the image. Must be between -100 and 100. - /// - /// is less than -100 or is greater than 100. - /// - public SaturationProcessor(int saturation) - { - this.Amount = saturation; - Guard.MustBeBetweenOrEqualTo(saturation, -100, 100, nameof(saturation)); - float saturationFactor = saturation / 100F; - - // Stop at -1 to prevent inversion. - saturationFactor++; - - // The matrix is set up to "shear" the color space using the following set of values. - // Note that each color component has an effective luminance which contributes to the - // overall brightness of the pixel. - // See http://graficaobscura.com/matrix/index.html - float saturationComplement = 1.0f - saturationFactor; - float saturationComplementR = 0.3086f * saturationComplement; - float saturationComplementG = 0.6094f * saturationComplement; - float saturationComplementB = 0.0820f * saturationComplement; - - var matrix4X4 = new Matrix4x4 - { - M11 = saturationComplementR + saturationFactor, - M12 = saturationComplementR, - M13 = saturationComplementR, - M21 = saturationComplementG, - M22 = saturationComplementG + saturationFactor, - M23 = saturationComplementG, - M31 = saturationComplementB, - M32 = saturationComplementB, - M33 = saturationComplementB + saturationFactor, - M44 = 1 - }; - - this.Matrix = matrix4X4; - } - - /// - /// Gets the amount to apply. - /// - public int Amount { get; } - - /// - public override Matrix4x4 Matrix { get; } - } -} diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs index d959ebf521..9cd7c6861e 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs @@ -1,35 +1,30 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Converts the colors of the image to their sepia equivalent. - /// The formula used matches the svg specification. + /// Applies a sepia filter matrix using the given amount. /// /// The pixel format. - internal class SepiaProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class SepiaProcessor : FilterProcessor + where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + /// The proportion of the conversion. Must be between 0 and 1. + public SepiaProcessor(float amount) + : base(Matrix4x4Extensions.CreateSepiaFilter(amount)) { - M11 = .393F, - M12 = .349F, - M13 = .272F, - M21 = .769F, - M22 = .686F, - M23 = .534F, - M31 = .189F, - M32 = .168F, - M33 = .131F, - M44 = 1 - }; + this.Amount = amount; + } - /// - public override bool Compand => false; + /// + /// Gets the proportion of the conversion + /// + public float Amount { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetector2DProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetector2DProcessor.cs index f93787d129..741a6e308c 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetector2DProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetector2DProcessor.cs @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.Grayscale) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorCompassProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorCompassProcessor.cs index 32c22a8ce9..0ffd7d48f5 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorCompassProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorCompassProcessor.cs @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.Grayscale) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorProcessor.cs index 3b98b77fc8..e5c5179716 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetection/EdgeDetectorProcessor.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.Grayscale) { - new GrayscaleBt709Processor().Apply(source, sourceRectangle, configuration); + new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); } } diff --git a/src/ImageSharp/Processing/Processors/Effects/AlphaProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/AlphaProcessor.cs deleted file mode 100644 index 7e5bd02abc..0000000000 --- a/src/ImageSharp/Processing/Processors/Effects/AlphaProcessor.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An to change the alpha component of an . - /// - /// The pixel format. - internal class AlphaProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The percentage to adjust the opacity of the image. Must be between 0 and 1. - /// - /// is less than 0 or is greater than 1. - /// - public AlphaProcessor(float percent) - { - Guard.MustBeBetweenOrEqualTo(percent, 0, 1, nameof(percent)); - this.Value = percent; - } - - /// - /// Gets the alpha value. - /// - public float Value { get; } - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - var alphaVector = new Vector4(1, 1, 1, this.Value); - - Parallel.For( - minY, - maxY, - configuration.ParallelOptions, - y => - { - Span row = source.GetPixelRowSpan(y - startY); - - for (int x = minX; x < maxX; x++) - { - ref TPixel pixel = ref row[x - startX]; - pixel.PackFromVector4(pixel.ToVector4() * alphaVector); - } - }); - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Effects/BrightnessProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/BrightnessProcessor.cs deleted file mode 100644 index c864330c9d..0000000000 --- a/src/ImageSharp/Processing/Processors/Effects/BrightnessProcessor.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An to change the brightness of an . - /// - /// The pixel format. - internal class BrightnessProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The new brightness of the image. Must be between -100 and 100. - /// - /// is less than -100 or is greater than 100. - /// - public BrightnessProcessor(int brightness) - { - Guard.MustBeBetweenOrEqualTo(brightness, -100, 100, nameof(brightness)); - this.Value = brightness; - } - - /// - /// Gets the brightness value. - /// - public int Value { get; } - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - float brightness = this.Value / 100F; - - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - Parallel.For( - minY, - maxY, - configuration.ParallelOptions, - y => - { - Span row = source.GetPixelRowSpan(y - startY); - - for (int x = minX; x < maxX; x++) - { - ref TPixel pixel = ref row[x - startX]; - - // TODO: Check this with other formats. - Vector4 vector = pixel.ToVector4().Expand(); - Vector3 transformed = new Vector3(vector.X, vector.Y, vector.Z) + new Vector3(brightness); - vector = new Vector4(transformed, vector.W); - - pixel.PackFromVector4(vector.Compress()); - } - }); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Effects/ContrastProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/ContrastProcessor.cs deleted file mode 100644 index 5ab2662110..0000000000 --- a/src/ImageSharp/Processing/Processors/Effects/ContrastProcessor.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An to change the contrast of an . - /// - /// The pixel format. - internal class ContrastProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The new contrast of the image. Must be between -100 and 100. - /// - /// is less than -100 or is greater than 100. - /// - public ContrastProcessor(int contrast) - { - Guard.MustBeBetweenOrEqualTo(contrast, -100, 100, nameof(contrast)); - this.Value = contrast; - } - - /// - /// Gets the contrast value. - /// - public int Value { get; } - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - float contrast = (100F + this.Value) / 100F; - - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - var contrastVector = new Vector4(contrast, contrast, contrast, 1); - var shiftVector = new Vector4(.5F, .5F, .5F, 1); - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - Parallel.For( - minY, - maxY, - configuration.ParallelOptions, - y => - { - Span row = source.GetPixelRowSpan(y - startY); - - for (int x = minX; x < maxX; x++) - { - ref TPixel pixel = ref row[x - startX]; - - Vector4 vector = pixel.ToVector4().Expand(); - vector -= shiftVector; - vector *= contrastVector; - vector += shiftVector; - - pixel.PackFromVector4(vector.Compress()); - } - }); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Effects/InvertProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/InvertProcessor.cs deleted file mode 100644 index 448025f70a..0000000000 --- a/src/ImageSharp/Processing/Processors/Effects/InvertProcessor.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An to invert the colors of an . - /// - /// The pixel format. - internal class InvertProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - Vector3 inverseVector = Vector3.One; - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - Parallel.For( - minY, - maxY, - configuration.ParallelOptions, - y => - { - Span row = source.GetPixelRowSpan(y - startY); - - for (int x = minX; x < maxX; x++) - { - ref TPixel pixel = ref row[x - startX]; - - var vector = pixel.ToVector4(); - Vector3 vector3 = inverseVector - new Vector3(vector.X, vector.Y, vector.Z); - - pixel.PackFromVector4(new Vector4(vector3, vector.W)); - } - }); - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 473bc2b523..eb1ae8fa37 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -47,6 +47,8 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = file.CreateImage()) { + image.Mutate(x => x.Saturate(1.5F, new Primitives.Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2))); + image.Save($"{path}/{file.FileName}"); } } diff --git a/tests/ImageSharp.Tests/Processing/Effects/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/BrightnessTest.cs similarity index 59% rename from tests/ImageSharp.Tests/Processing/Effects/BrightnessTest.cs rename to tests/ImageSharp.Tests/Processing/ColorMatrix/BrightnessTest.cs index d057f9233a..05605767fb 100644 --- a/tests/ImageSharp.Tests/Processing/Effects/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/ColorMatrix/BrightnessTest.cs @@ -13,19 +13,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Effects [Fact] public void Brightness_amount_BrightnessProcessorDefaultsSet() { - this.operations.Brightness(23); - var processor = this.Verify>(); + this.operations.Brightness(1.5F); + BrightnessProcessor processor = this.Verify>(); - Assert.Equal(23, processor.Value); + Assert.Equal(1.5F, processor.Amount); } [Fact] public void Brightness_amount_rect_BrightnessProcessorDefaultsSet() { - this.operations.Brightness(23, this.rect); - var processor = this.Verify>(this.rect); + this.operations.Brightness(1.5F, this.rect); + BrightnessProcessor processor = this.Verify>(this.rect); - Assert.Equal(23, processor.Value); + Assert.Equal(1.5F, processor.Amount); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Effects/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/ContrastTest.cs similarity index 55% rename from tests/ImageSharp.Tests/Processing/Effects/ContrastTest.cs rename to tests/ImageSharp.Tests/Processing/ColorMatrix/ContrastTest.cs index 374937ea38..4aec24dad0 100644 --- a/tests/ImageSharp.Tests/Processing/Effects/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/ColorMatrix/ContrastTest.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Effects @@ -13,19 +11,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Effects [Fact] public void Contrast_amount_ContrastProcessorDefaultsSet() { - this.operations.Contrast(23); - var processor = this.Verify>(); + this.operations.Contrast(1.5F); + ContrastProcessor processor = this.Verify>(); - Assert.Equal(23, processor.Value); + Assert.Equal(1.5F, processor.Amount); } [Fact] public void Contrast_amount_rect_ContrastProcessorDefaultsSet() { - this.operations.Contrast(23, this.rect); - var processor = this.Verify>(this.rect); + this.operations.Contrast(1.5F, this.rect); + ContrastProcessor processor = this.Verify>(this.rect); - Assert.Equal(23, processor.Value); + Assert.Equal(1.5F, processor.Amount); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs index 0ec03dfdd5..b8da495cac 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix this.operations.Hue(34f); var processor = this.Verify>(); - Assert.Equal(34f, processor.Angle); + Assert.Equal(34f, processor.Degrees); } [Fact] @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix this.operations.Hue(5f, this.rect); var processor = this.Verify>(this.rect); - Assert.Equal(5f, processor.Angle); + Assert.Equal(5f, processor.Degrees); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Effects/InvertTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/InvertTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/Effects/InvertTest.cs rename to tests/ImageSharp.Tests/Processing/ColorMatrix/InvertTest.cs diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs new file mode 100644 index 0000000000..4108cbddac --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Effects +{ + public class OpacityTest : BaseImageOperationsExtensionTest + { + [Fact] + public void Alpha_amount_AlphaProcessorDefaultsSet() + { + this.operations.Opacity(0.2f); + OpacityProcessor processor = this.Verify>(); + + Assert.Equal(.2f, processor.Amount); + } + + [Fact] + public void Alpha_amount_rect_AlphaProcessorDefaultsSet() + { + this.operations.Opacity(0.6f, this.rect); + OpacityProcessor processor = this.Verify>(this.rect); + + Assert.Equal(.6f, processor.Amount); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturationTest.cs b/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs similarity index 58% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/SaturationTest.cs rename to tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs index 5a44023293..abac9fc1cc 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturationTest.cs +++ b/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs @@ -1,21 +1,19 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix { - public class SaturationTest : BaseImageOperationsExtensionTest + public class SaturateTest : BaseImageOperationsExtensionTest { [Fact] public void Saturation_amount_SaturationProcessorDefaultsSet() { - this.operations.Saturation(34); - var processor = this.Verify>(); + this.operations.Saturate(34); + SaturateProcessor processor = this.Verify>(); Assert.Equal(34, processor.Amount); } @@ -23,8 +21,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix [Fact] public void Saturation_amount_rect_SaturationProcessorDefaultsSet() { - this.operations.Saturation(5, this.rect); - var processor = this.Verify>(this.rect); + this.operations.Saturate(5, this.rect); + SaturateProcessor processor = this.Verify>(this.rect); Assert.Equal(5, processor.Amount); } diff --git a/tests/ImageSharp.Tests/Processing/Effects/AlphaTest.cs b/tests/ImageSharp.Tests/Processing/Effects/AlphaTest.cs deleted file mode 100644 index 9840d71c7a..0000000000 --- a/tests/ImageSharp.Tests/Processing/Effects/AlphaTest.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors; -using SixLabors.Primitives; -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Processing.Effects -{ - public class AlphaTest : BaseImageOperationsExtensionTest - { - [Fact] - public void Alpha_amount_AlphaProcessorDefaultsSet() - { - this.operations.Alpha(0.2f); - var processor = this.Verify>(); - - Assert.Equal(.2f, processor.Value); - } - - [Fact] - public void Alpha_amount_rect_AlphaProcessorDefaultsSet() - { - this.operations.Alpha(0.6f, this.rect); - var processor = this.Verify>(this.rect); - - Assert.Equal(.6f, processor.Value); - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Effects/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs similarity index 83% rename from tests/ImageSharp.Tests/Processing/Processors/Effects/BrightnessTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs index 9bfed05b95..44804d0b26 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Effects/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs @@ -11,16 +11,16 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { public class BrightnessTest : FileTestBase { - public static readonly TheoryData BrightnessValues - = new TheoryData + public static readonly TheoryData BrightnessValues + = new TheoryData { - 50, - -50 + .5F, + 1.5F }; [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(BrightnessValues), DefaultPixelType)] - public void ImageShouldApplyBrightnessFilter(TestImageProvider provider, int value) + public void ImageShouldApplyBrightnessFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -32,11 +32,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(BrightnessValues), DefaultPixelType)] - public void ImageShouldApplyBrightnessFilterInBox(TestImageProvider provider, int value) + public void ImageShouldApplyBrightnessFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Effects/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs similarity index 83% rename from tests/ImageSharp.Tests/Processing/Processors/Effects/ContrastTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs index f1e33db88b..9117ff1b9f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Effects/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs @@ -11,16 +11,16 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { public class ContrastTest : FileTestBase { - public static readonly TheoryData ContrastValues - = new TheoryData + public static readonly TheoryData ContrastValues + = new TheoryData { - 50, - -50 + .5F, + 1.5F }; [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(ContrastValues), DefaultPixelType)] - public void ImageShouldApplyContrastFilter(TestImageProvider provider, int value) + public void ImageShouldApplyContrastFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -32,11 +32,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(ContrastValues), DefaultPixelType)] - public void ImageShouldApplyContrastFilterInBox(TestImageProvider provider, int value) + public void ImageShouldApplyContrastFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Effects/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs similarity index 96% rename from tests/ImageSharp.Tests/Processing/Processors/Effects/InvertTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs index 5930236774..9536b36f16 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Effects/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Effects/AlphaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs similarity index 87% rename from tests/ImageSharp.Tests/Processing/Processors/Effects/AlphaTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs index 7f2840f2c8..2815233f24 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Effects/AlphaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { - public class AlphaTest : FileTestBase + public class OpacityTest : FileTestBase { public static readonly TheoryData AlphaValues = new TheoryData @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Alpha(value)); + image.Mutate(x => x.Opacity(value)); image.DebugSave(provider, value); } } @@ -36,11 +36,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Alpha(value, bounds)); + image.Mutate(x => x.Opacity(value, bounds)); image.DebugSave(provider, value); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturationTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs similarity index 74% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturationTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs index 2532a7fe79..4ef39a8abc 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturationTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs @@ -9,38 +9,38 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix { - public class SaturationTest : FileTestBase + public class SaturateTest : FileTestBase { - public static readonly TheoryData SaturationValues - = new TheoryData + public static readonly TheoryData SaturationValues + = new TheoryData { - 50 , - -50 , + .5f, + 1.5F, }; [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(SaturationValues), DefaultPixelType)] - public void ImageShouldApplySaturationFilter(TestImageProvider provider, int value) + public void ImageShouldApplySaturationFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Saturation(value)); + image.Mutate(x => x.Saturate(value)); image.DebugSave(provider, value); } } [Theory] [WithFileCollection(nameof(DefaultFiles), nameof(SaturationValues), DefaultPixelType)] - public void ImageShouldApplySaturationFilterInBox(TestImageProvider provider, int value) + public void ImageShouldApplySaturationFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Saturation(value, bounds)); + image.Mutate(x => x.Saturate(value, bounds)); image.DebugSave(provider, value); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs index 0a550a3c1a..dde34fcc43 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Tests { if (pngColorType != PngColorType.RgbWithAlpha) { - sourceImage.Mutate(c => c.Alpha(1)); + sourceImage.Mutate(c => c.Opacity(1)); } var encoder = new PngEncoder() { PngColorType = pngColorType }; @@ -93,12 +93,12 @@ namespace SixLabors.ImageSharp.Tests using (Image original = provider.GetImage()) { - original.Mutate(c => c.Alpha(1)); + original.Mutate(c => c.Opacity(1)); using (var sdBitmap = new System.Drawing.Bitmap(path)) { using (Image resaved = SystemDrawingBridge.FromFromRgb24SystemDrawingBitmap(sdBitmap)) { - resaved.Mutate(c => c.Alpha(1)); + resaved.Mutate(c => c.Opacity(1)); ImageComparer comparer = ImageComparer.Exact; comparer.VerifySimilarity(original, resaved); } From 32dfb7cb611050cc02e69ce14e8f8ee67277fc31 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 14:07:26 +1100 Subject: [PATCH 39/99] Convert photo effects --- .../Processing/ColorMatrix/BlackWhite.cs | 2 +- .../ColorMatrix/Matrix4x4Extensions.cs | 72 +++++++++++++++++++ .../ColorMatrix/BlackWhiteProcessor.cs | 29 +++----- .../ColorMatrix/KodachromeProcessor.cs | 23 +++--- .../ColorMatrix/LomographProcessor.cs | 15 +--- .../ColorMatrix/PolaroidProcessor.cs | 24 +------ 6 files changed, 96 insertions(+), 69 deletions(-) diff --git a/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs b/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs index 300c073818..a8c946a736 100644 --- a/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs +++ b/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs @@ -43,4 +43,4 @@ namespace SixLabors.ImageSharp return source; } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs b/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs index 21c7f05e59..37aa0f6297 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs @@ -12,6 +12,78 @@ namespace SixLabors.ImageSharp.Processing // ReSharper disable once InconsistentNaming public static class Matrix4x4Extensions { + /// + /// Gets an approximated black and white filter + /// + public static Matrix4x4 BlackWhiteFilter { get; } = new Matrix4x4() + { + M11 = 1.5F, + M12 = 1.5F, + M13 = 1.5F, + M21 = 1.5F, + M22 = 1.5F, + M23 = 1.5F, + M31 = 1.5F, + M32 = 1.5F, + M33 = 1.5F, + M41 = -1F, + M42 = -1F, + M43 = -1F, + M44 = 1 + }; + + /// + /// Gets a filter recreating an old Kodachrome camera effect. + /// + public static Matrix4x4 KodachromeFilter { get; } = new Matrix4x4 + { + M11 = 0.7297023F, + M22 = 0.6109577F, + M33 = 0.597218F, + M41 = 0.105F, + M42 = 0.145F, + M43 = 0.155F, + M44 = 1 + } + + * CreateSaturateFilter(1.2F) * CreateContrastFilter(1.35F); + + /// + /// Gets a filter recreating an old Lomograph camera effect. + /// + public static Matrix4x4 LomographFilter { get; } = new Matrix4x4 + { + M11 = 1.5F, + M22 = 1.45F, + M33 = 1.16F, + M41 = -.1F, + M42 = -.02F, + M43 = -.07F, + M44 = 1 + } + + * CreateSaturateFilter(1.1F) * CreateContrastFilter(1.33F); + + /// + /// Gets a filter recreating an old Polaroid camera effect. + /// + public static Matrix4x4 PolaroidFilter { get; } = new Matrix4x4 + { + M11 = 1.538F, + M12 = -0.062F, + M13 = -0.262F, + M21 = -0.022F, + M22 = 1.578F, + M23 = -0.022F, + M31 = .216F, + M32 = -.16F, + M33 = 1.5831F, + M41 = 0.02F, + M42 = -0.05F, + M43 = -0.05F, + M44 = 1 + }; + /// /// Create a brightness filter matrix using the given amount. /// diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs index 9f81273433..0745b7ba70 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs @@ -1,34 +1,23 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Converts the colors of the image to their black and white equivalent. + /// Applies a black and white filter matrix to the image /// /// The pixel format. - internal class BlackWhiteProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class BlackWhiteProcessor : FilterProcessor + where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4() + /// + /// Initializes a new instance of the class. + /// + public BlackWhiteProcessor() + : base(Matrix4x4Extensions.BlackWhiteFilter) { - M11 = 1.5F, - M12 = 1.5F, - M13 = 1.5F, - M21 = 1.5F, - M22 = 1.5F, - M23 = 1.5F, - M31 = 1.5F, - M32 = 1.5F, - M33 = 1.5F, - M41 = -1F, - M42 = -1F, - M43 = -1F, - M44 = 1 - }; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs index 4277e1fc2e..1f644247b9 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs @@ -1,28 +1,23 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Converts the colors of the image recreating an old Kodachrome camera effect. + /// Applies a filter matrix recreating an old Kodachrome camera effect matrix to the image /// /// The pixel format. - internal class KodachromeProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel + internal class KodachromeProcessor : FilterProcessor + where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public KodachromeProcessor() + : base(Matrix4x4Extensions.KodachromeFilter) { - M11 = 0.6997023F, - M22 = 0.4609577F, - M33 = 0.397218F, - M41 = 0.005F, - M42 = -0.005F, - M43 = 0.005F, - M44 = 1 - }; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs index 1ec76bf554..62e9e7d624 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating an old Lomograph effect. /// /// The pixel format. - internal class LomographProcessor : ColorMatrixProcessor + internal class LomographProcessor : FilterProcessor where TPixel : struct, IPixel { private static readonly TPixel VeryDarkGreen = ColorBuilder.FromRGBA(0, 10, 0, 255); @@ -22,22 +22,11 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The options effecting blending and composition. public LomographProcessor(GraphicsOptions options) + : base(Matrix4x4Extensions.LomographFilter) { this.options = options; } - /// - public override Matrix4x4 Matrix => new Matrix4x4 - { - M11 = 1.5F, - M22 = 1.45F, - M33 = 1.11F, - M41 = -.1F, - M42 = .0F, - M43 = -.08F, - M44 = 1 - }; - /// protected override void AfterApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs index f910562e64..60a48e1315 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs +++ b/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -11,11 +10,11 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating an old Polaroid effect. /// /// The pixel format. - internal class PolaroidProcessor : ColorMatrixProcessor + internal class PolaroidProcessor : FilterProcessor where TPixel : struct, IPixel { private static readonly TPixel VeryDarkOrange = ColorBuilder.FromRGB(102, 34, 0); - private static readonly TPixel LightOrange = ColorBuilder.FromRGBA(255, 153, 102, 178); + private static readonly TPixel LightOrange = ColorBuilder.FromRGBA(255, 153, 102, 128); private readonly GraphicsOptions options; /// @@ -23,28 +22,11 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The options effecting blending and composition. public PolaroidProcessor(GraphicsOptions options) + : base(Matrix4x4Extensions.PolaroidFilter) { this.options = options; } - /// - public override Matrix4x4 Matrix => new Matrix4x4 - { - M11 = 1.538F, - M12 = -0.062F, - M13 = -0.262F, - M21 = -0.022F, - M22 = 1.578F, - M23 = -0.022F, - M31 = .216F, - M32 = -.16F, - M33 = 1.5831F, - M41 = 0.02F, - M42 = -0.05F, - M43 = -0.05F, - M44 = 1 - }; - /// protected override void AfterApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { From f16e4bd609e72cbb5a38ee32af13106245c80b09 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 15:42:20 +1100 Subject: [PATCH 40/99] Add Filter extension method + rename --- .../Processing/ColorMatrix/BlackWhite.cs | 2 - .../Processing/ColorMatrix/Filter.cs | 47 +++++++ ...atrix4x4Extensions.cs => MatrixFilters.cs} | 121 +++++++++++++++++- .../ColorBlindness/AchromatomalyProcessor.cs | 34 ----- .../ColorBlindness/AchromatopsiaProcessor.cs | 34 ----- .../ColorBlindness/ProtanomalyProcessor.cs | 31 ----- .../ColorMatrix/ColorMatrixProcessor.cs | 78 ----------- .../ColorMatrix/IColorMatrixProcessor.cs | 27 ---- .../BlackWhiteProcessor.cs | 2 +- .../BrightnessProcessor.cs | 2 +- .../ColorBlindness/AchromatomalyProcessor.cs | 23 ++++ .../ColorBlindness/AchromatopsiaProcessor.cs | 23 ++++ .../ColorBlindness/DeuteranomalyProcessor.cs | 22 +--- .../ColorBlindness/DeuteranopiaProcessor.cs | 22 +--- .../ColorBlindness/ProtanomalyProcessor.cs | 23 ++++ .../ColorBlindness/ProtanopiaProcessor.cs | 21 +-- .../ColorBlindness/README.md | 0 .../ColorBlindness/TritanomalyProcessor.cs | 22 +--- .../ColorBlindness/TritanopiaProcessor.cs | 22 +--- .../ContrastProcessor.cs | 2 +- .../FilterProcessor.cs | 0 .../GrayscaleBt601Processor.cs | 2 +- .../GrayscaleBt709Processor.cs | 2 +- .../{ColorMatrix => Filters}/HueProcessor.cs | 2 +- .../InvertProcessor.cs | 2 +- .../KodachromeProcessor.cs | 2 +- .../LomographProcessor.cs | 2 +- .../OpacityProcessor.cs | 2 +- .../PolaroidProcessor.cs | 2 +- .../SaturateProcessor.cs | 2 +- .../SepiaProcessor.cs | 2 +- .../BlackWhiteTest.cs | 8 +- .../BrightnessTest.cs | 0 .../ColorBlindnessTest.cs | 2 +- .../{ColorMatrix => Filters}/ContrastTest.cs | 0 .../Processing/Filters/FilterTest.cs | 26 ++++ .../{ColorMatrix => Filters}/GrayscaleTest.cs | 2 +- .../{ColorMatrix => Filters}/HueTest.cs | 2 +- .../{ColorMatrix => Filters}/InvertTest.cs | 0 .../KodachromeTest.cs | 2 +- .../{ColorMatrix => Filters}/LomographTest.cs | 0 .../{ColorMatrix => Filters}/OpacityTest.cs | 0 .../{ColorMatrix => Filters}/PolaroidTest.cs | 2 +- .../{ColorMatrix => Filters}/SaturateTest.cs | 2 +- .../{ColorMatrix => Filters}/SepiaTest.cs | 2 +- .../Processors/ColorMatrix/BlackWhiteTest.cs | 2 +- .../ColorMatrix/ColorBlindnessTest.cs | 2 +- .../Processors/ColorMatrix/FilterTest.cs | 44 +++++++ .../Processors/ColorMatrix/GrayscaleTest.cs | 2 +- .../Processors/ColorMatrix/HueTest.cs | 2 +- .../Processors/ColorMatrix/KodachromeTest.cs | 2 +- .../Processors/ColorMatrix/LomographTest.cs | 2 +- .../Processors/ColorMatrix/PolaroidTest.cs | 2 +- .../Processors/ColorMatrix/SaturateTest.cs | 2 +- .../Processors/ColorMatrix/SepiaTest.cs | 2 +- 55 files changed, 372 insertions(+), 316 deletions(-) create mode 100644 src/ImageSharp/Processing/ColorMatrix/Filter.cs rename src/ImageSharp/Processing/ColorMatrix/{Matrix4x4Extensions.cs => MatrixFilters.cs} (79%) delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/ColorMatrix/IColorMatrixProcessor.cs rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/BlackWhiteProcessor.cs (92%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/BrightnessProcessor.cs (94%) create mode 100644 src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatomalyProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatopsiaProcessor.cs rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/DeuteranomalyProcessor.cs (50%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/DeuteranopiaProcessor.cs (51%) create mode 100644 src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanomalyProcessor.cs rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/ProtanopiaProcessor.cs (53%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/README.md (100%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/TritanomalyProcessor.cs (50%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ColorBlindness/TritanopiaProcessor.cs (50%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/ContrastProcessor.cs (94%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/FilterProcessor.cs (100%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/GrayscaleBt601Processor.cs (93%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/GrayscaleBt709Processor.cs (93%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/HueProcessor.cs (93%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/InvertProcessor.cs (93%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/KodachromeProcessor.cs (92%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/LomographProcessor.cs (95%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/OpacityProcessor.cs (93%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/PolaroidProcessor.cs (96%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/SaturateProcessor.cs (94%) rename src/ImageSharp/Processing/Processors/{ColorMatrix => Filters}/SepiaProcessor.cs (93%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/BlackWhiteTest.cs (65%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/BrightnessTest.cs (100%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/ColorBlindnessTest.cs (97%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/ContrastTest.cs (100%) create mode 100644 tests/ImageSharp.Tests/Processing/Filters/FilterTest.cs rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/GrayscaleTest.cs (96%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/HueTest.cs (93%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/InvertTest.cs (100%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/KodachromeTest.cs (92%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/LomographTest.cs (100%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/OpacityTest.cs (100%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/PolaroidTest.cs (92%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/SaturateTest.cs (93%) rename tests/ImageSharp.Tests/Processing/{ColorMatrix => Filters}/SepiaTest.cs (92%) create mode 100644 tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs diff --git a/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs b/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs index a8c946a736..d64db34bad 100644 --- a/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs +++ b/src/ImageSharp/Processing/ColorMatrix/BlackWhite.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; diff --git a/src/ImageSharp/Processing/ColorMatrix/Filter.cs b/src/ImageSharp/Processing/ColorMatrix/Filter.cs new file mode 100644 index 0000000000..7eb684978a --- /dev/null +++ b/src/ImageSharp/Processing/ColorMatrix/Filter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Filters an image but the given color matrix + /// + /// The pixel format. + /// The image this method extends. + /// The filter color matrix + /// The . + public static IImageProcessingContext Filter(this IImageProcessingContext source, Matrix4x4 matrix) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new FilterProcessor(matrix)); + return source; + } + + /// + /// Filters an image but the given color matrix + /// + /// The pixel format. + /// The image this method extends. + /// The filter color matrix + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Filter(this IImageProcessingContext source, Matrix4x4 matrix, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new FilterProcessor(matrix), rectangle); + return source; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs b/src/ImageSharp/Processing/ColorMatrix/MatrixFilters.cs similarity index 79% rename from src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs rename to src/ImageSharp/Processing/ColorMatrix/MatrixFilters.cs index 37aa0f6297..8cbc21b2a6 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Matrix4x4Extensions.cs +++ b/src/ImageSharp/Processing/ColorMatrix/MatrixFilters.cs @@ -9,9 +9,126 @@ namespace SixLabors.ImageSharp.Processing /// /// Provides extensions methods for the struct /// - // ReSharper disable once InconsistentNaming - public static class Matrix4x4Extensions + public static class MatrixFilters { + /// + /// Gets a filter recreating Achromatomaly (Color desensitivity) color blindness + /// + public static Matrix4x4 AchromatomalyFilter { get; } = new Matrix4x4 + { + M11 = .618F, + M12 = .163F, + M13 = .163F, + M21 = .320F, + M22 = .775F, + M23 = .320F, + M31 = .062F, + M32 = .062F, + M33 = .516F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Achromatopsia (Monochrome) color blindness. + /// + public static Matrix4x4 AchromatopsiaFilter { get; } = new Matrix4x4 + { + M11 = .299F, + M12 = .299F, + M13 = .299F, + M21 = .587F, + M22 = .587F, + M23 = .587F, + M31 = .114F, + M32 = .114F, + M33 = .114F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Deuteranomaly (Green-Weak) color blindness. + /// + public static Matrix4x4 DeuteranomalyFilter { get; } = new Matrix4x4 + { + M11 = 0.8F, + M12 = 0.258F, + M21 = 0.2F, + M22 = 0.742F, + M23 = 0.142F, + M33 = 0.858F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Deuteranopia (Green-Blind) color blindness. + /// + public static Matrix4x4 DeuteranopiaFilter { get; } = new Matrix4x4 + { + M11 = 0.625F, + M12 = 0.7F, + M21 = 0.375F, + M22 = 0.3F, + M23 = 0.3F, + M33 = 0.7F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Protanomaly (Red-Weak) color blindness. + /// + public static Matrix4x4 ProtanomalyFilter { get; } = new Matrix4x4 + { + M11 = 0.817F, + M12 = 0.333F, + M21 = 0.183F, + M22 = 0.667F, + M23 = 0.125F, + M33 = 0.875F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Protanopia (Red-Blind) color blindness. + /// + public static Matrix4x4 ProtanopiaFilter { get; } = new Matrix4x4 + { + M11 = 0.567F, + M12 = 0.558F, + M21 = 0.433F, + M22 = 0.442F, + M23 = 0.242F, + M33 = 0.758F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Tritanomaly (Blue-Weak) color blindness. + /// + public static Matrix4x4 TritanomalyFilter { get; } = new Matrix4x4 + { + M11 = 0.967F, + M21 = 0.33F, + M22 = 0.733F, + M23 = 0.183F, + M32 = 0.267F, + M33 = 0.817F, + M44 = 1 + }; + + /// + /// Gets a filter recreating Tritanopia (Blue-Blind) color blindness. + /// + public static Matrix4x4 TritanopiaFilter { get; } = new Matrix4x4 + { + M11 = 0.95F, + M21 = 0.05F, + M22 = 0.433F, + M23 = 0.475F, + M32 = 0.567F, + M33 = 0.525F, + M44 = 1 + }; + /// /// Gets an approximated black and white filter /// diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs deleted file mode 100644 index 91e5c7f68f..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatomalyProcessor.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Converts the colors of the image recreating Achromatomaly (Color desensitivity) color blindness. - /// - /// The pixel format. - internal class AchromatomalyProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel - { - /// - public override Matrix4x4 Matrix => new Matrix4x4 - { - M11 = .618F, - M12 = .163F, - M13 = .163F, - M21 = .320F, - M22 = .775F, - M23 = .320F, - M31 = .062F, - M32 = .062F, - M33 = .516F, - M44 = 1 - }; - - /// - public override bool Compand => false; - } -} diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs deleted file mode 100644 index 0d6578852c..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/AchromatopsiaProcessor.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Converts the colors of the image recreating Achromatopsia (Monochrome) color blindness. - /// - /// The pixel format. - internal class AchromatopsiaProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel - { - /// - public override Matrix4x4 Matrix => new Matrix4x4 - { - M11 = .299F, - M12 = .299F, - M13 = .299F, - M21 = .587F, - M22 = .587F, - M23 = .587F, - M31 = .114F, - M32 = .114F, - M33 = .114F, - M44 = 1 - }; - - /// - public override bool Compand => false; - } -} diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs deleted file mode 100644 index 4e32cb5298..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanomalyProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Converts the colors of the image recreating Protanopia (Red-Weak) color blindness. - /// - /// The pixel format. - internal class ProtanomalyProcessor : ColorMatrixProcessor - where TPixel : struct, IPixel - { - /// - public override Matrix4x4 Matrix => new Matrix4x4 - { - M11 = 0.817F, - M12 = 0.333F, - M21 = 0.183F, - M22 = 0.667F, - M23 = 0.125F, - M33 = 0.875F, - M44 = 1 - }; - - /// - public override bool Compand => false; - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs deleted file mode 100644 index 4a64bfaa0d..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// The color matrix filter. Inherit from this class to perform operation involving color matrices. - /// - /// The pixel format. - internal abstract class ColorMatrixProcessor : ImageProcessor, IColorMatrixProcessor - where TPixel : struct, IPixel - { - /// - public abstract Matrix4x4 Matrix { get; } - - /// - public virtual bool Compand { get; set; } = true; - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - Matrix4x4 matrix = this.Matrix; - bool compand = this.Compand; - - Parallel.For( - minY, - maxY, - configuration.ParallelOptions, - y => - { - Span row = source.GetPixelRowSpan(y - startY); - - for (int x = minX; x < maxX; x++) - { - ref TPixel pixel = ref row[x - startX]; - var vector = pixel.ToVector4(); - - if (compand) - { - vector = vector.Expand(); - } - - vector = Vector4.Transform(vector, matrix); - pixel.PackFromVector4(compand ? vector.Compress() : vector); - } - }); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/IColorMatrixProcessor.cs b/src/ImageSharp/Processing/Processors/ColorMatrix/IColorMatrixProcessor.cs deleted file mode 100644 index 84e7461b56..0000000000 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/IColorMatrixProcessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Encapsulates properties and methods for creating processors that utilize a matrix to - /// alter the image pixels. - /// - /// The pixel format. - internal interface IColorMatrixProcessor : IImageProcessor - where TPixel : struct, IPixel - { - /// - /// Gets the used to alter the image. - /// - Matrix4x4 Matrix { get; } - - /// - /// Gets or sets a value indicating whether to compress or expand individual pixel color values on processing. - /// - bool Compand { get; set; } - } -} diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs similarity index 92% rename from src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs index 0745b7ba70..30fcfab4fd 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/BlackWhiteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// public BlackWhiteProcessor() - : base(Matrix4x4Extensions.BlackWhiteFilter) + : base(MatrixFilters.BlackWhiteFilter) { } } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs similarity index 94% rename from src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs index 051a74ba25..b1a68a9c91 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/BrightnessProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/BrightnessProcessor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be greater than or equal to 0. public BrightnessProcessor(float amount) - : base(Matrix4x4Extensions.CreateBrightnessFilter(amount)) + : base(MatrixFilters.CreateBrightnessFilter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatomalyProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatomalyProcessor.cs new file mode 100644 index 0000000000..8d9bf98579 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatomalyProcessor.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Converts the colors of the image recreating Achromatomaly (Color desensitivity) color blindness. + /// + /// The pixel format. + internal class AchromatomalyProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + public AchromatomalyProcessor() + : base(MatrixFilters.AchromatomalyFilter) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatopsiaProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatopsiaProcessor.cs new file mode 100644 index 0000000000..f19c55933d --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/AchromatopsiaProcessor.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Converts the colors of the image recreating Achromatopsia (Monochrome) color blindness. + /// + /// The pixel format. + internal class AchromatopsiaProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + public AchromatopsiaProcessor() + : base(MatrixFilters.AchromatopsiaFilter) + { + } + } +} diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranomalyProcessor.cs similarity index 50% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranomalyProcessor.cs index c4bb41ceb3..20a1d4ab46 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranomalyProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranomalyProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors @@ -10,22 +9,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating Deuteranomaly (Green-Weak) color blindness. /// /// The pixel format. - internal class DeuteranomalyProcessor : ColorMatrixProcessor + internal class DeuteranomalyProcessor : FilterProcessor where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public DeuteranomalyProcessor() + : base(MatrixFilters.DeuteranomalyFilter) { - M11 = 0.8F, - M12 = 0.258F, - M21 = 0.2F, - M22 = 0.742F, - M23 = 0.142F, - M33 = 0.858F, - M44 = 1 - }; - - /// - public override bool Compand => false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranopiaProcessor.cs similarity index 51% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranopiaProcessor.cs index 598af12ff0..e5e0225718 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/DeuteranopiaProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/DeuteranopiaProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors @@ -10,22 +9,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating Deuteranopia (Green-Blind) color blindness. /// /// The pixel format. - internal class DeuteranopiaProcessor : ColorMatrixProcessor + internal class DeuteranopiaProcessor : FilterProcessor where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public DeuteranopiaProcessor() + : base(MatrixFilters.DeuteranopiaFilter) { - M11 = 0.625F, - M12 = 0.7F, - M21 = 0.375F, - M22 = 0.3F, - M23 = 0.3F, - M33 = 0.7F, - M44 = 1 - }; - - /// - public override bool Compand => false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanomalyProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanomalyProcessor.cs new file mode 100644 index 0000000000..b7b61d5e59 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanomalyProcessor.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Converts the colors of the image recreating Protanomaly (Red-Weak) color blindness. + /// + /// The pixel format. + internal class ProtanomalyProcessor : FilterProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + public ProtanomalyProcessor() + : base(MatrixFilters.ProtanomalyFilter) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanopiaProcessor.cs similarity index 53% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanopiaProcessor.cs index d49b4a2cc0..54753f5b57 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/ProtanopiaProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/ProtanopiaProcessor.cs @@ -10,22 +10,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating Protanopia (Red-Blind) color blindness. /// /// The pixel format. - internal class ProtanopiaProcessor : ColorMatrixProcessor + internal class ProtanopiaProcessor : FilterProcessor where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public ProtanopiaProcessor() + : base(MatrixFilters.ProtanopiaFilter) { - M11 = 0.567F, - M12 = 0.558F, - M21 = 0.433F, - M22 = 0.442F, - M23 = 0.242F, - M33 = 0.758F, - M44 = 1 - }; - - /// - public override bool Compand => false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/README.md b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/README.md similarity index 100% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/README.md rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/README.md diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanomalyProcessor.cs similarity index 50% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanomalyProcessor.cs index d34f22343c..57f4d4fa83 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanomalyProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanomalyProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors @@ -10,22 +9,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating Tritanomaly (Blue-Weak) color blindness. /// /// The pixel format. - internal class TritanomalyProcessor : ColorMatrixProcessor + internal class TritanomalyProcessor : FilterProcessor where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public TritanomalyProcessor() + : base(MatrixFilters.TritanomalyFilter) { - M11 = 0.967F, - M21 = 0.33F, - M22 = 0.733F, - M23 = 0.183F, - M32 = 0.267F, - M33 = 0.817F, - M44 = 1 - }; - - /// - public override bool Compand => false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanopiaProcessor.cs similarity index 50% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanopiaProcessor.cs index 453ac99a72..b03a18cf76 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ColorBlindness/TritanopiaProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ColorBlindness/TritanopiaProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors @@ -10,22 +9,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Converts the colors of the image recreating Tritanopia (Blue-Blind) color blindness. /// /// The pixel format. - internal class TritanopiaProcessor : ColorMatrixProcessor + internal class TritanopiaProcessor : FilterProcessor where TPixel : struct, IPixel { - /// - public override Matrix4x4 Matrix => new Matrix4x4 + /// + /// Initializes a new instance of the class. + /// + public TritanopiaProcessor() + : base(MatrixFilters.TritanopiaFilter) { - M11 = 0.95F, - M21 = 0.05F, - M22 = 0.433F, - M23 = 0.475F, - M32 = 0.567F, - M33 = 0.525F, - M44 = 1 - }; - - /// - public override bool Compand => false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs similarity index 94% rename from src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs index a46d0c1dee..8ebeb939fb 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/ContrastProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/ContrastProcessor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be greater than or equal to 0. public ContrastProcessor(float amount) - : base(Matrix4x4Extensions.CreateContrastFilter(amount)) + : base(MatrixFilters.CreateContrastFilter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/ColorMatrix/FilterProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs rename to src/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs index fde0665d30..7ea52dcb92 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt601Processor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt601Processor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be between 0 and 1. public GrayscaleBt601Processor(float amount) - : base(Matrix4x4Extensions.CreateGrayscaleBt601Filter(amount)) + : base(MatrixFilters.CreateGrayscaleBt601Filter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs rename to src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs index 92195a30da..2d97f65842 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/GrayscaleBt709Processor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be between 0 and 1. public GrayscaleBt709Processor(float amount) - : base(Matrix4x4Extensions.CreateGrayscaleBt709Filter(amount)) + : base(MatrixFilters.CreateGrayscaleBt709Filter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/HueProcessor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/HueProcessor.cs index edb61855e9..302314db40 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/HueProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/HueProcessor.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The angle of rotation in degrees public HueProcessor(float degrees) - : base(Matrix4x4Extensions.CreateHueFilter(degrees)) + : base(MatrixFilters.CreateHueFilter(degrees)) { this.Degrees = degrees; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs index 1a78c22b51..e258e9d96e 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/InvertProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/InvertProcessor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be between 0 and 1. public InvertProcessor(float amount) - : base(Matrix4x4Extensions.CreateInvertFilter(amount)) + : base(MatrixFilters.CreateInvertFilter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs similarity index 92% rename from src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs index 1f644247b9..6f27a04538 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/KodachromeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/KodachromeProcessor.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// public KodachromeProcessor() - : base(Matrix4x4Extensions.KodachromeFilter) + : base(MatrixFilters.KodachromeFilter) { } } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs similarity index 95% rename from src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs index 62e9e7d624..5ea57fd27b 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/LomographProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/LomographProcessor.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The options effecting blending and composition. public LomographProcessor(GraphicsOptions options) - : base(Matrix4x4Extensions.LomographFilter) + : base(MatrixFilters.LomographFilter) { this.options = options; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs index 7247cec409..1c0d2600ea 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/OpacityProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/OpacityProcessor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be between 0 and 1. public OpacityProcessor(float amount) - : base(Matrix4x4Extensions.CreateOpacityFilter(amount)) + : base(MatrixFilters.CreateOpacityFilter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs similarity index 96% rename from src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs index 60a48e1315..5491db7efe 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/PolaroidProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/PolaroidProcessor.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The options effecting blending and composition. public PolaroidProcessor(GraphicsOptions options) - : base(Matrix4x4Extensions.PolaroidFilter) + : base(MatrixFilters.PolaroidFilter) { this.options = options; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs similarity index 94% rename from src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs index 0a25161a88..44b3fe3ced 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/SaturateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/SaturateProcessor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be greater than or equal to 0. public SaturateProcessor(float amount) - : base(Matrix4x4Extensions.CreateSaturateFilter(amount)) + : base(MatrixFilters.CreateSaturateFilter(amount)) { this.Amount = amount; } diff --git a/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs rename to src/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs index 9cd7c6861e..b30d0fe052 100644 --- a/src/ImageSharp/Processing/Processors/ColorMatrix/SepiaProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/SepiaProcessor.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The proportion of the conversion. Must be between 0 and 1. public SepiaProcessor(float amount) - : base(Matrix4x4Extensions.CreateSepiaFilter(amount)) + : base(MatrixFilters.CreateSepiaFilter(amount)) { this.Amount = amount; } diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Filters/BlackWhiteTest.cs similarity index 65% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/BlackWhiteTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/BlackWhiteTest.cs index f6efdc9a99..4ade9fe64b 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/BlackWhiteTest.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; -using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class BlackWhiteTest : BaseImageOperationsExtensionTest { @@ -14,14 +12,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix public void BlackWhite_CorrectProcessor() { this.operations.BlackWhite(); - var p = this.Verify>(); + BlackWhiteProcessor p = this.Verify>(); } [Fact] public void BlackWhite_rect_CorrectProcessor() { this.operations.BlackWhite( this.rect); - var p = this.Verify>(this.rect); + BlackWhiteProcessor p = this.Verify>(this.rect); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/BrightnessTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Filters/ColorBlindnessTest.cs similarity index 97% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/ColorBlindnessTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/ColorBlindnessTest.cs index cc80e32d58..2dc695f560 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/ColorBlindnessTest.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class ColorBlindnessTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Filters/ContrastTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/ContrastTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/ContrastTest.cs diff --git a/tests/ImageSharp.Tests/Processing/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Filters/FilterTest.cs new file mode 100644 index 0000000000..d1e9c0ba0a --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Filters/FilterTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Filters +{ + public class FilterTest : BaseImageOperationsExtensionTest + { + [Fact] + public void Filter_CorrectProcessor() + { + this.operations.Filter(MatrixFilters.AchromatomalyFilter * MatrixFilters.CreateHueFilter(90F)); + FilterProcessor p = this.Verify>(); + } + + [Fact] + public void Filter_rect_CorrectProcessor() + { + this.operations.Filter(MatrixFilters.AchromatomalyFilter * MatrixFilters.CreateHueFilter(90F), this.rect); + FilterProcessor p = this.Verify>(this.rect); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Filters/GrayscaleTest.cs similarity index 96% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/GrayscaleTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/GrayscaleTest.cs index 14697c6234..80c377eb1d 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/GrayscaleTest.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class GrayscaleTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs b/tests/ImageSharp.Tests/Processing/Filters/HueTest.cs similarity index 93% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/HueTest.cs index b8da495cac..bf03ee4f81 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/HueTest.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class HueTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Filters/InvertTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/InvertTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/InvertTest.cs diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Filters/KodachromeTest.cs similarity index 92% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/KodachromeTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/KodachromeTest.cs index 61a4124c9e..b3731d43c1 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/KodachromeTest.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class KodachromeTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Filters/LomographTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/LomographTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/LomographTest.cs diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Filters/OpacityTest.cs similarity index 100% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/OpacityTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/OpacityTest.cs diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Filters/PolaroidTest.cs similarity index 92% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/PolaroidTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/PolaroidTest.cs index 10433aa9b1..5661851a56 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/PolaroidTest.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class PolaroidTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Filters/SaturateTest.cs similarity index 93% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/SaturateTest.cs index abac9fc1cc..e3b9e3d3b2 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/SaturateTest.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Processing.Processors; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class SaturateTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/ColorMatrix/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Filters/SepiaTest.cs similarity index 92% rename from tests/ImageSharp.Tests/Processing/ColorMatrix/SepiaTest.cs rename to tests/ImageSharp.Tests/Processing/Filters/SepiaTest.cs index b2117b94a2..4983313882 100644 --- a/tests/ImageSharp.Tests/Processing/ColorMatrix/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/SepiaTest.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Filters { public class SepiaTest : BaseImageOperationsExtensionTest { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs index c3250ccfc0..c0481809af 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class BlackWhiteTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs index 122ff5a640..cbc4a2810e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs @@ -8,7 +8,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class ColorBlindnessTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs new file mode 100644 index 0000000000..cae8d12964 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters +{ + public class FilterTest : FileTestBase + { + [Theory] + [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + public void ImageShouldApplyFilter(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.Filter(MatrixFilters.CreateBrightnessFilter(1.2F) * MatrixFilters.CreateContrastFilter(1.2F))); + image.DebugSave(provider); + } + } + + [Theory] + [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + public void ImageShouldApplyFilterInBox(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image source = provider.GetImage()) + using (Image image = source.Clone()) + { + var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + + image.Mutate(x => x.Filter(MatrixFilters.CreateBrightnessFilter(1.2F) * MatrixFilters.CreateContrastFilter(1.2F), bounds)); + image.DebugSave(provider); + + ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs index 0fbc54b8f9..c870659a6b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class GrayscaleTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs index 5a1ea16a7e..743d46efaa 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class HueTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs index ebf163ab8d..587ff0c013 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class KodachromeTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs index 48a58580a1..7087ac7b98 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class LomographTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs index 2e61e3f02b..57d6cdd1df 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class PolaroidTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs index 4ef39a8abc..b9e2c3f0f7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class SaturateTest : FileTestBase { diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs index 28b1f6256a..71a35a773d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.ColorMatrix +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { public class SepiaTest : FileTestBase { From 493d582d25b6add13a1c8a4c6181019fda9664a8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 15:42:34 +1100 Subject: [PATCH 41/99] Remove methods from test --- tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index eb1ae8fa37..473bc2b523 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -47,8 +47,6 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = file.CreateImage()) { - image.Mutate(x => x.Saturate(1.5F, new Primitives.Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2))); - image.Save($"{path}/{file.FileName}"); } } From f723b62aeec4eb241a78eaea49db8ce66d3dfa9b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 16:02:05 +1100 Subject: [PATCH 42/99] Temp disable edge detection verification --- .../Processing/Convolution/DetectEdgesTest.cs | 22 +++++++++++-------- .../Processors/ColorMatrix/FilterTest.cs | 6 ++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs index 5a711bd04e..b52938aac1 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs @@ -18,18 +18,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution public void DetectEdges_SobelProcessorDefaultsSet() { this.operations.DetectEdges(); - var processor = this.Verify>(); - Assert.True(processor.Grayscale); + // TODO: Enable once we have updated the images + // SobelProcessor processor = this.Verify>(); + // Assert.True(processor.Grayscale); } [Fact] public void DetectEdges_Rect_SobelProcessorDefaultsSet() { this.operations.DetectEdges(this.rect); - var processor = this.Verify>(this.rect); - Assert.True(processor.Grayscale); + // TODO: Enable once we have updated the images + // SobelProcessor processor = this.Verify>(this.rect); + // Assert.True(processor.Grayscale); } public static IEnumerable EdgeDetectionTheoryData => new[] { new object[]{ new TestType>(), EdgeDetection.Kayyali }, @@ -50,9 +52,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution where TProcessor : IEdgeDetectorProcessor { this.operations.DetectEdges(filter); - var processor = this.Verify(); - Assert.True(processor.Grayscale); + // TODO: Enable once we have updated the images + // var processor = this.Verify(); + // Assert.True(processor.Grayscale); } [Theory] @@ -60,11 +63,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution public void DetectEdges_filter_grayscale_SobelProcessorDefaultsSet(TestType type, EdgeDetection filter) where TProcessor : IEdgeDetectorProcessor { - var grey = (int)filter % 2 == 0; + bool grey = (int)filter % 2 == 0; this.operations.DetectEdges(filter, grey); - var processor = this.Verify(); - Assert.Equal(grey, processor.Grayscale); + // TODO: Enable once we have updated the images + // var processor = this.Verify() + // Assert.Equal(grey, processor.Grayscale); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs index cae8d12964..80447ebf8b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -19,7 +20,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Filter(MatrixFilters.CreateBrightnessFilter(1.2F) * MatrixFilters.CreateContrastFilter(1.2F))); + Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); + Matrix4x4 contrast = MatrixFilters.CreateContrastFilter(1.2F); + Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); + image.Mutate(x => x.Filter(brightness * contrast * saturation)); image.DebugSave(provider); } } From a1d9ac6a3749e26a3cfad3cc26fd0716aaab693b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 18 Dec 2017 16:12:53 +1100 Subject: [PATCH 43/99] Disable other edge detection verification --- .../Processors/Convolution/DetectEdgesTest.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index 54686cb4cc..c2d8916384 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -39,7 +39,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { image.Mutate(x => x.DetectEdges(detector)); image.DebugSave(provider, detector.ToString()); - image.CompareToReferenceOutput(provider, detector.ToString()); + + // TODO: Enable once we have updated the images + // image.CompareToReferenceOutput(provider, detector.ToString()); } } @@ -52,7 +54,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { image.Mutate(x => x.DetectEdges()); image.DebugSave(provider); - image.CompareToReferenceOutput(provider); + + // TODO: Enable once we have updated the images + // image.CompareToReferenceOutput(provider); } } @@ -79,7 +83,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution image.Mutate(x => x.DetectEdges(bounds)); image.DebugSave(provider); - image.CompareToReferenceOutput(provider); + + // TODO: Enable once we have updated the images + //image.CompareToReferenceOutput(provider); // TODO: We don't need this any longer after switching to ReferenceImages //ImageComparer.Tolerant().EnsureProcessorChangesAreConstrained(source, image, bounds); From 478b79b0e99b0618a70bc9bcd964c71b9b4c7472 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 20 Dec 2017 18:35:30 +0100 Subject: [PATCH 44/99] more/better TransformTests --- .../Processing/Transforms/TransformTests.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 54a3af36fd..59a16eb78d 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -10,6 +10,8 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { + using SixLabors.ImageSharp.Helpers; + public class TransformTests { private readonly ITestOutputHelper Output; @@ -17,7 +19,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public static readonly TheoryData TransformValues = new TheoryData { + { 0, 1, 1, 0, 0 }, + { 50, 1, 1, 0, 0 }, + { 0, 1, 1, 20, 10 }, { 50, 1, 1, 20, 10 }, + { 0, 1, 1, -20, -10 }, { 50, 1, 1, -20, -10 }, { 50, 1.5f, 1.5f, 0, 0 }, { 0, 2f, 1f, 0, 0 }, @@ -75,7 +81,30 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms [Theory] [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] - public void Transform_RotateScaleTranslate( + public void Transform_RotateScaleTranslate_AutoDestRectangle( + TestImageProvider provider, + float angleDeg, + float sx, float sy, + float tx, float ty) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); + var translate = Matrix3x2.CreateTranslation(tx, ty); + var scale = Matrix3x2.CreateScale(sx, sy); + Matrix3x2 m = rotate * scale * translate; + + this.PrintMatrix(m); + + image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); + } + } + + [Theory] + [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] + public void Transform_RotateScaleTranslate_SameDestRectangle( TestImageProvider provider, float angleDeg, float sx, float sy, @@ -89,13 +118,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var scale = Matrix3x2.CreateScale(sx, sy); Matrix3x2 m = rotate * scale * translate; - this.Output.WriteLine(m.ToString()); + this.PrintMatrix(m); - image.Mutate(i => i.Transform(m)); + Rectangle destBounds = image.Bounds(); + image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic, destBounds)); image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); } } + [Theory] [WithTestPatternImages(nameof(ResamplerNames), 100, 200, PixelTypes.Rgba32)] public void Transform_WithSampler(TestImageProvider provider, string resamplerName) @@ -141,5 +172,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(white, rgba.Rgb); } } + + private void PrintMatrix(Matrix3x2 a) + { + string s = $"{a.M11:F10},{a.M12:F10},{a.M21:F10},{a.M22:F10},{a.M31:F10},{a.M32:F10}"; + this.Output.WriteLine(s); + } } } \ No newline at end of file From 7d5c77cc7cbe0f3c9f30ebfe795585de13b633d8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 21 Dec 2017 12:21:30 +1100 Subject: [PATCH 45/99] Update default behaviours --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 29 ------------- .../Processors/Transforms/AffineProcessor.cs | 16 +++++-- .../Transforms/CenteredAffineProcessor.cs | 8 ++++ .../Processors/Transforms/RotateProcessor.cs | 4 +- .../Processors/Transforms/SkewProcessor.cs | 2 +- .../Processing/Transforms/Rotate.cs | 2 +- src/ImageSharp/Processing/Transforms/Skew.cs | 2 +- .../Processing/Transforms/TransformHelpers.cs | 43 +++++++++++++++++++ .../Processing/Transforms/TransformTests.cs | 9 +++- 9 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 src/ImageSharp/Processing/Transforms/TransformHelpers.cs diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index a1c83415bf..75c9190d24 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -139,35 +139,6 @@ namespace SixLabors.ImageSharp return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); } - /// - /// Gets the bounding from the given matrix. - /// - /// The source rectangle. - /// The transformation matrix. - /// - /// The . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) - { - // Calculate the position of the four corners in world space by applying - // The world matrix to the four corners in object space (0, 0, width, height) - var tl = Vector2.Transform(Vector2.Zero, matrix); - var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); - var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); - var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); - - // Find the minimum and maximum "corners" based on the ones above - float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); - float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); - float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); - float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); - float sizeX = maxX - minX + .5F; - float sizeY = maxY - minY + .5F; - - return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY)); - } - /// /// Finds the bounding rectangle based on the first instance of any color component other /// than the given one. diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index ad8221b88b..59b8442639 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -67,9 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { if (this.targetRectangle == Rectangle.Empty) { - this.targetRectangle = Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; + this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix); } // We will always be creating the clone even for mutate because we may need to resize the canvas @@ -148,6 +146,7 @@ namespace SixLabors.ImageSharp.Processing.Processors Vector2 maxXY = point + radius; Vector2 minXY = point - radius; + // max, maxY, minX, minY var extents = new Vector4( MathF.Floor(maxXY.X + .5F), MathF.Floor(maxXY.Y + .5F), @@ -245,6 +244,17 @@ namespace SixLabors.ImageSharp.Processing.Processors return this.TransformMatrix; } + /// + /// Gets the bounding relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// The + protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix3x2 matrix) + { + return sourceRectangle; + } + /// /// Calculated the weights for the given point. /// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs index 5f03f94e27..5631af3aa0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs @@ -27,5 +27,13 @@ namespace SixLabors.ImageSharp.Processing.Processors var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter; } + + /// + protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix3x2 matrix) + { + return Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix) + ? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 7af1c68f12..fa47dadaaa 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The angle of rotation in degrees. public RotateProcessor(float degrees) - : this(degrees, KnownResamplers.NearestNeighbor) + : this(degrees, KnownResamplers.Bicubic) { } @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { degrees = degrees % 360; - if (degrees < 0) + while (degrees < 0) { degrees += 360; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 07f082838c..b123a309be 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The angle in degrees to perform the skew along the x-axis. /// The angle in degrees to perform the skew along the y-axis. public SkewProcessor(float degreesX, float degreesY) - : this(degreesX, degreesY, KnownResamplers.NearestNeighbor) + : this(degreesX, degreesY, KnownResamplers.Bicubic) { } diff --git a/src/ImageSharp/Processing/Transforms/Rotate.cs b/src/ImageSharp/Processing/Transforms/Rotate.cs index e9ae4fcf32..69fb7ebf0c 100644 --- a/src/ImageSharp/Processing/Transforms/Rotate.cs +++ b/src/ImageSharp/Processing/Transforms/Rotate.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Rotate(this IImageProcessingContext source, float degrees) where TPixel : struct, IPixel - => Rotate(source, degrees, KnownResamplers.NearestNeighbor); + => Rotate(source, degrees, KnownResamplers.Bicubic); /// /// Rotates an image by the given angle in degrees using the specified sampling algorithm. diff --git a/src/ImageSharp/Processing/Transforms/Skew.cs b/src/ImageSharp/Processing/Transforms/Skew.cs index b7a431cce4..0613a690b8 100644 --- a/src/ImageSharp/Processing/Transforms/Skew.cs +++ b/src/ImageSharp/Processing/Transforms/Skew.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Skew(this IImageProcessingContext source, float degreesX, float degreesY) where TPixel : struct, IPixel - => Skew(source, degreesX, degreesY, KnownResamplers.NearestNeighbor); + => Skew(source, degreesX, degreesY, KnownResamplers.Bicubic); /// /// Skews an image by the given angles in degrees using the specified sampling algorithm. diff --git a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs new file mode 100644 index 0000000000..6f9560a9fa --- /dev/null +++ b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp +{ + /// + /// Contains helper methods for working with affine transforms + /// + public class TransformHelpers + { + /// + /// Returns the bounding relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) + { + // Calculate the position of the four corners in world space by applying + // The world matrix to the four corners in object space (0, 0, width, height) + var tl = Vector2.Transform(Vector2.Zero, matrix); + var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); + var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); + var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); + + // Find the minimum and maximum "corners" based on the ones above + float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); + float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); + float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); + float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); + float sizeX = maxX - minX + .5F; + float sizeY = maxY - minY + .5F; + + return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 59a16eb78d..79a1fd4454 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -72,7 +72,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F)); var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F); - image.Mutate(c => c.Transform(rotate * translate, resampler)); + Rectangle sourceRectangle = image.Bounds(); + Matrix3x2 matrix = rotate * translate; + + Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix); + + image.Mutate(c => c.Transform(matrix, resampler, destRectangle)); image.DebugSave(provider, resamplerName); VerifyAllPixelsAreWhiteOrTransparent(image); @@ -96,7 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Matrix3x2 m = rotate * scale * translate; this.PrintMatrix(m); - + image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); } From 95fdde505efe9041826a41a3ee85b31d734fc4f3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 21 Dec 2017 12:23:08 +1100 Subject: [PATCH 46/99] Use Bicubic for TransformProcessor --- .../Processing/Processors/Transforms/TransformProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index 577691cbb5..140fd7ec6a 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The transformation matrix public TransformProcessor(Matrix3x2 matrix) - : this(matrix, KnownResamplers.NearestNeighbor) + : this(matrix, KnownResamplers.Bicubic) { } From 3ce951d0008a9f7f19ede28e3df929d0f5977887 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 21 Dec 2017 02:45:39 +0100 Subject: [PATCH 47/99] Transform_RotateScale_ManuallyCentered --- .../Processing/Transforms/TransformTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 59a16eb78d..cfc59870ea 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -26,6 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { 0, 1, 1, -20, -10 }, { 50, 1, 1, -20, -10 }, { 50, 1.5f, 1.5f, 0, 0 }, + { 50, 1.1F, 1.2F, 20, 10 }, { 0, 2f, 1f, 0, 0 }, { 0, 1f, 2f, 0, 0 }, }; @@ -126,6 +127,28 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms } } + [Theory] + [WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)] + public void Transform_RotateScale_ManuallyCentered(TestImageProvider provider, float angleDeg, float s) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); + Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height); + var translate = Matrix3x2.CreateTranslation(-toCenter); + var translateBack = Matrix3x2.CreateTranslation(toCenter); + var scale = Matrix3x2.CreateScale(s); + + Matrix3x2 m = translate * rotate * scale * translateBack; + + this.PrintMatrix(m); + + Rectangle destBounds = image.Bounds(); + image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic, destBounds)); + image.DebugSave(provider, $"R({angleDeg})_S({s})"); + } + } [Theory] [WithTestPatternImages(nameof(ResamplerNames), 100, 200, PixelTypes.Rgba32)] From 38aa608f44a1ff7a30393213f08cdd0c23ce5605 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 21 Dec 2017 02:53:12 +0100 Subject: [PATCH 48/99] drop unnecessary "AutoDestRectangle" tests --- .../Processing/Transforms/TransformTests.cs | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index d7220f72ca..9cb7890fe2 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms [Theory] [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] - public void Transform_RotateScaleTranslate_AutoDestRectangle( + public void Transform_RotateScaleTranslate( TestImageProvider provider, float angleDeg, float sx, float sy, @@ -107,31 +107,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); } } - - [Theory] - [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] - public void Transform_RotateScaleTranslate_SameDestRectangle( - TestImageProvider provider, - float angleDeg, - float sx, float sy, - float tx, float ty) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) - { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); - var translate = Matrix3x2.CreateTranslation(tx, ty); - var scale = Matrix3x2.CreateScale(sx, sy); - Matrix3x2 m = rotate * scale * translate; - - this.PrintMatrix(m); - - Rectangle destBounds = image.Bounds(); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic, destBounds)); - image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); - } - } - + [Theory] [WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)] public void Transform_RotateScale_ManuallyCentered(TestImageProvider provider, float angleDeg, float s) From 83221617166219001e4193034985ff8f1e6f38bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 22 Dec 2017 01:08:27 +1100 Subject: [PATCH 49/99] Use test pattern files for tests --- .../Processing/ColorMatrix/Grayscale.cs | 2 +- .../{ColorMatrix => Filters}/BlackWhiteTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/BrightnessTest.cs | 6 +++--- .../ColorBlindnessTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/ContrastTest.cs | 6 +++--- .../{ColorMatrix => Filters}/FilterTest.cs | 15 +++++++++------ .../{ColorMatrix => Filters}/GrayscaleTest.cs | 7 ++++--- .../{ColorMatrix => Filters}/HueTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/InvertTest.cs | 6 +++--- .../{ColorMatrix => Filters}/KodachromeTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/LomographTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/OpacityTest.cs | 6 +++--- .../{ColorMatrix => Filters}/PolaroidTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/SaturateTest.cs | 8 ++++---- .../{ColorMatrix => Filters}/SepiaTest.cs | 8 ++++---- 15 files changed, 58 insertions(+), 54 deletions(-) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/BlackWhiteTest.cs (80%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/BrightnessTest.cs (83%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/ColorBlindnessTest.cs (84%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/ContrastTest.cs (83%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/FilterTest.cs (66%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/GrayscaleTest.cs (87%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/HueTest.cs (80%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/InvertTest.cs (84%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/KodachromeTest.cs (80%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/LomographTest.cs (80%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/OpacityTest.cs (84%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/PolaroidTest.cs (80%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/SaturateTest.cs (82%) rename tests/ImageSharp.Tests/Processing/Processors/{ColorMatrix => Filters}/SepiaTest.cs (80%) diff --git a/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs b/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs index 4fa80a183f..ee43d5b016 100644 --- a/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs +++ b/src/ImageSharp/Processing/ColorMatrix/Grayscale.cs @@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp /// The . public static IImageProcessingContext Grayscale(this IImageProcessingContext source, GrayscaleMode mode, Rectangle rectangle) where TPixel : struct, IPixel - => Grayscale(source, GrayscaleMode.Bt709, 1F, rectangle); + => Grayscale(source, mode, 1F, rectangle); /// /// Applies Grayscale toning to the image using the given amount. diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs index c0481809af..601f30a79e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class BlackWhiteTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyBlackWhiteFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyBlackWhiteFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.BlackWhite(bounds)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs similarity index 83% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index 44804d0b26..b0a830b9d9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(BrightnessValues), DefaultPixelType)] + [WithTestPatternImages(nameof(BrightnessValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyBrightnessFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { @@ -31,14 +31,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(BrightnessValues), DefaultPixelType)] + [WithTestPatternImages(nameof(BrightnessValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyBrightnessFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Brightness(value, bounds)); image.DebugSave(provider, value); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs similarity index 84% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index cbc4a2810e..2342fe932d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(ColorBlindnessFilters), DefaultPixelType)] + [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] public void ImageShouldApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { @@ -38,14 +38,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(ColorBlindnessFilters), DefaultPixelType)] + [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] public void ImageShouldApplyColorBlindnessFilterInBox(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.ColorBlindness(colorBlindness, bounds)); image.DebugSave(provider, colorBlindness.ToString()); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs similarity index 83% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs index 9117ff1b9f..67b86788aa 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(ContrastValues), DefaultPixelType)] + [WithTestPatternImages(nameof(ContrastValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyContrastFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { @@ -31,14 +31,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(ContrastValues), DefaultPixelType)] + [WithTestPatternImages(nameof(ContrastValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyContrastFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Contrast(value, bounds)); image.DebugSave(provider, value); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs similarity index 66% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index 80447ebf8b..9053308680 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -14,31 +14,34 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class FilterTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) { Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); - Matrix4x4 contrast = MatrixFilters.CreateContrastFilter(1.2F); + Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); - image.Mutate(x => x.Filter(brightness * contrast * saturation)); + image.Mutate(x => x.Filter(brightness * hue * saturation)); image.DebugSave(provider); } } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Filter(MatrixFilters.CreateBrightnessFilter(1.2F) * MatrixFilters.CreateContrastFilter(1.2F), bounds)); + Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); + Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); + Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); + image.Mutate(x => x.Filter(brightness * hue * saturation, bounds)); image.DebugSave(provider); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs similarity index 87% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs index c870659a6b..23d514f352 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters /// Use test patterns over loaded images to save decode time. /// [Theory] - [WithTestPatternImages(nameof(GrayscaleModeTypes), 50, 50, DefaultPixelType)] - public void ImageShouldApplyGrayscaleFilterAll(TestImageProvider provider, GrayscaleMode value) + [WithTestPatternImages(nameof(GrayscaleModeTypes), 100, 100, DefaultPixelType)] + public void ImageShouldApplyGrayscaleFilter(TestImageProvider provider, GrayscaleMode value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(nameof(GrayscaleModeTypes), 50, 50, DefaultPixelType)] + [WithTestPatternImages(nameof(GrayscaleModeTypes), 100, 100, DefaultPixelType)] public void ImageShouldApplyGrayscaleFilterInBox(TestImageProvider provider, GrayscaleMode value) where TPixel : struct, IPixel { @@ -53,6 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters using (Image image = source.Clone()) { var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); + image.Mutate(x => x.Grayscale(value, bounds)); image.DebugSave(provider, value.ToString()); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs index 743d46efaa..5a34595a62 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(HueValues), DefaultPixelType)] + [WithTestPatternImages(nameof(HueValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyHueFilter(TestImageProvider provider, int value) where TPixel : struct, IPixel { @@ -31,14 +31,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(HueValues), DefaultPixelType)] + [WithTestPatternImages(nameof(HueValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyHueFilterInBox(TestImageProvider provider, int value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Hue(value, bounds)); image.DebugSave(provider, value); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs similarity index 84% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs index 9536b36f16..2199e691fa 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects public class InvertTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyInvertFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyInvertFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Invert(bounds)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs index 587ff0c013..6d95baaef0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class KodachromeTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyKodachromeFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyKodachromeFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Kodachrome(bounds)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 7087ac7b98..2f9cd4319b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class LomographTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyLomographFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyLomographFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Lomograph(bounds)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs similarity index 84% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs index 2815233f24..12bf93299a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/OpacityTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(AlphaValues), DefaultPixelType)] + [WithTestPatternImages(nameof(AlphaValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyAlphaFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { @@ -31,14 +31,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(AlphaValues), DefaultPixelType)] + [WithTestPatternImages(nameof(AlphaValues), 100, 100, DefaultPixelType)] public void ImageShouldApplyAlphaFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Opacity(value, bounds)); image.DebugSave(provider, value); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs index 57d6cdd1df..44e69c09ee 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class PolaroidTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyPolaroidFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplyPolaroidFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Polaroid(bounds)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs similarity index 82% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs index b9e2c3f0f7..8553cad4c2 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs @@ -14,12 +14,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public static readonly TheoryData SaturationValues = new TheoryData { - .5f, + .5F, 1.5F, }; [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(SaturationValues), DefaultPixelType)] + [WithTestPatternImages(nameof(SaturationValues), 100, 100, DefaultPixelType)] public void ImageShouldApplySaturationFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { @@ -31,14 +31,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), nameof(SaturationValues), DefaultPixelType)] + [WithTestPatternImages(nameof(SaturationValues), 100, 100, DefaultPixelType)] public void ImageShouldApplySaturationFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Saturate(value, bounds)); image.DebugSave(provider, value); diff --git a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs similarity index 80% rename from tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs rename to tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs index 71a35a773d..499567ae88 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/ColorMatrix/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public class SepiaTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplySepiaFilter(TestImageProvider provider) where TPixel : struct, IPixel { @@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] + [WithTestPatternImages(100, 100, DefaultPixelType)] public void ImageShouldApplySepiaFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) - using (var image = source.Clone()) + using (Image image = source.Clone()) { - var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); + var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); image.Mutate(x => x.Sepia(bounds)); image.DebugSave(provider); From 6f4aa808cbc71ec655167e8dfe838920d1c5303b Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 23 Dec 2017 23:56:23 +0100 Subject: [PATCH 50/99] Transform_IntoRectangle --- .../Processing/Transforms/TransformTests.cs | 72 +++++++++++++---- .../WithSolidFilledImagesAttribute.cs | 80 ++++++++++++++++++- .../WithTestPatternImageAttribute.cs | 2 +- 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 9cb7890fe2..ac1e3812dc 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -16,6 +16,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { private readonly ITestOutputHelper Output; + /// + /// angleDeg, sx, sy, tx, ty + /// public static readonly TheoryData TransformValues = new TheoryData { @@ -51,6 +54,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms nameof(KnownResamplers.Welch), }; + public static readonly TheoryData Transform_DoesNotCreateEdgeArtifacts_ResamplerNames = + new TheoryData + { + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Lanczos8), + }; + public TransformTests(ITestOutputHelper output) { this.Output = output; @@ -60,10 +72,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms /// The output of an "all white" image should be "all white" or transparent, regardless of the transformation and the resampler. /// [Theory] - [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor))] - [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Triangle))] - [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic))] - [WithSolidFilledImages(5, 5, 255, 255, 255, 255, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos8))] + [WithSolidFilledImages(nameof(Transform_DoesNotCreateEdgeArtifacts_ResamplerNames), 5, 5, 255, 255, 255, 255, PixelTypes.Rgba32)] public void Transform_DoesNotCreateEdgeArtifacts(TestImageProvider provider, string resamplerName) where TPixel : struct, IPixel { @@ -115,19 +124,36 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); - Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height); - var translate = Matrix3x2.CreateTranslation(-toCenter); - var translateBack = Matrix3x2.CreateTranslation(toCenter); - var scale = Matrix3x2.CreateScale(s); + Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image); - Matrix3x2 m = translate * rotate * scale * translateBack; + image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.DebugSave(provider, $"R({angleDeg})_S({s})"); + } + } - this.PrintMatrix(m); + public static readonly TheoryData Transform_IntoRectangle_Data = + new TheoryData + { + { 0, 0, 10, 10 }, + { 0, 0, 5, 10 }, + { 0, 0, 10, 5 }, + {-5,-5, 15, 15 } + }; - Rectangle destBounds = image.Bounds(); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic, destBounds)); - image.DebugSave(provider, $"R({angleDeg})_S({s})"); + [Theory] + [WithSolidFilledImages(nameof(Transform_IntoRectangle_Data), 10, 10, nameof(Rgba32.Red), PixelTypes.Rgba32)] + public void Transform_IntoRectangle(TestImageProvider provider, int x0, int y0, int w, int h) + where TPixel : struct, IPixel + { + var rectangle = new Rectangle(x0, y0, w, h); + + using (Image image = provider.GetImage()) + { + Matrix3x2 m = this.MakeManuallyCenteredMatrix(45, 0.8f, image); + + image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + string testDetails = $"({x0},{y0}-W{w},H{h})"; + image.DebugSave(provider, testDetails); } } @@ -142,13 +168,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(50); Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); var translate = Matrix3x2.CreateTranslation(75, 0); - - + image.Mutate(i => i.Transform(rotate * scale * translate, sampler)); image.DebugSave(provider, resamplerName); } } + private Matrix3x2 MakeManuallyCenteredMatrix(float angleDeg, float s, Image image) + where TPixel : struct, IPixel + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); + Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height); + var translate = Matrix3x2.CreateTranslation(-toCenter); + var translateBack = Matrix3x2.CreateTranslation(toCenter); + var scale = Matrix3x2.CreateScale(s); + + Matrix3x2 m = translate * rotate * scale * translateBack; + + this.PrintMatrix(m); + return m; + } + private static IResampler GetResampler(string name) { PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs index f787a35916..991f7108fe 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs @@ -6,6 +6,8 @@ using System.Reflection; namespace SixLabors.ImageSharp.Tests { + using SixLabors.ImageSharp.PixelFormats; + /// /// Triggers passing instances which produce an image of size width * height filled with the requested color. /// One instance will be passed for each the pixel format defined by the pixelTypes parameter @@ -56,14 +58,88 @@ namespace SixLabors.ImageSharp.Tests byte a, PixelTypes pixelTypes, params object[] additionalParameters) - : base(width, height, pixelTypes, additionalParameters) + : this(null, width, height, r, g, b, a, pixelTypes, additionalParameters) + { + } + + /// + /// Triggers passing instances which produce an image of size width * height filled with the requested color. + /// One instance will be passed for each the pixel format defined by the pixelTypes parameter + /// + /// The member data to apply to theories + /// The width of the requested image + /// The height of the requested image + /// Red + /// Green + /// Blue + /// /// Alpha + /// The requested pixel types + /// Additional theory parameter values + public WithSolidFilledImagesAttribute( + string memberData, + int width, + int height, + byte r, + byte g, + byte b, + byte a, + PixelTypes pixelTypes, + params object[] additionalParameters) + : base(memberData, width, height, pixelTypes, additionalParameters) { this.R = r; this.G = g; this.B = b; this.A = a; } - + + /// + /// Triggers passing instances which produce an image of size width * height filled with the requested color. + /// One instance will be passed for each the pixel format defined by the pixelTypes parameter + /// + /// The width of the requested image + /// The height of the requested image + /// The referenced color name (name of property in + /// The requested pixel types + /// Additional theory parameter values + public WithSolidFilledImagesAttribute( + int width, + int height, + string colorName, + PixelTypes pixelTypes, + params object[] additionalParameters) + : this(null, width, height, colorName, pixelTypes, additionalParameters) + { + } + + /// + /// Triggers passing instances which produce an image of size width * height filled with the requested color. + /// One instance will be passed for each the pixel format defined by the pixelTypes parameter + /// + /// The member data to apply to theories + /// The width of the requested image + /// The height of the requested image + /// The referenced color name (name of property in + /// The requested pixel types + /// Additional theory parameter values + public WithSolidFilledImagesAttribute( + string memberData, + int width, + int height, + string colorName, + PixelTypes pixelTypes, + params object[] additionalParameters) + : base(memberData, width, height, pixelTypes, additionalParameters) + { + Guard.NotNull(colorName, nameof(colorName)); + + var c = (Rgba32)typeof(Rgba32).GetTypeInfo().GetField(colorName).GetValue(null); + this.R = c.R; + this.G = c.G; + this.B = c.B; + this.A = c.A; + } + /// /// Red /// diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs index 585bb8f066..7c659c64fc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests /// The requested parameter /// Additional theory parameter values public WithTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters) - : this(null, width, height, pixelTypes,additionalParameters) + : this(null, width, height, pixelTypes, additionalParameters) { } From 3c2e10bd14067718e48f25eaef91f4bee31ee73a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 24 Dec 2017 03:35:11 +0100 Subject: [PATCH 51/99] clean up filter tests + smaller output images --- .../Processors/Filters/BlackWhiteProcessor.cs | 2 +- .../Processing/Processors/Filters/BlackWhiteTest.cs | 10 +++++----- .../Processing/Processors/Filters/BrightnessTest.cs | 10 +++++----- .../Processors/Filters/ColorBlindnessTest.cs | 4 ++-- .../Processing/Processors/Filters/ContrastTest.cs | 10 +++++----- .../Processing/Processors/Filters/FilterTest.cs | 12 +++++++----- .../Processing/Processors/Filters/GrayscaleTest.cs | 10 +++++----- .../Processing/Processors/Filters/HueTest.cs | 10 +++++----- .../Processing/Processors/Filters/InvertTest.cs | 10 +++++----- .../Processing/Processors/Filters/KodachromeTest.cs | 10 +++++----- .../Processing/Processors/Filters/LomographTest.cs | 10 +++++----- .../Processing/Processors/Filters/OpacityTest.cs | 10 +++++----- .../Processing/Processors/Filters/PolaroidTest.cs | 10 +++++----- .../Processing/Processors/Filters/SaturateTest.cs | 10 +++++----- .../Processing/Processors/Filters/SepiaTest.cs | 10 +++++----- 15 files changed, 70 insertions(+), 68 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs index 30fcfab4fd..141dc493a7 100644 --- a/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Applies a black and white filter matrix to the image /// /// The pixel format. - internal class BlackWhiteProcessor : FilterProcessor + internal class BlackWhiteProcessor : FilterProcessor where TPixel : struct, IPixel { /// diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs index 601f30a79e..2b9620ed5e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class BlackWhiteTest : FileTestBase + public class BlackWhiteTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyBlackWhiteFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyBlackWhiteFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyBlackWhiteFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyBlackWhiteFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index b0a830b9d9..00bc605478 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { - public class BrightnessTest : FileTestBase + public class BrightnessTest { public static readonly TheoryData BrightnessValues = new TheoryData @@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithTestPatternImages(nameof(BrightnessValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyBrightnessFilter(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(BrightnessValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyBrightnessFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithTestPatternImages(nameof(BrightnessValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyBrightnessFilterInBox(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(BrightnessValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyBrightnessFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index 2342fe932d..36f4a31407 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters [Theory] [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] - public void ImageShouldApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindness colorBlindness) + public void ApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters [Theory] [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] - public void ImageShouldApplyColorBlindnessFilterInBox(TestImageProvider provider, ColorBlindness colorBlindness) + public void ApplyColorBlindnessFilterInBox(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs index 67b86788aa..caa09c579e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { - public class ContrastTest : FileTestBase + public class ContrastTest { public static readonly TheoryData ContrastValues = new TheoryData @@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithTestPatternImages(nameof(ContrastValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyContrastFilter(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(ContrastValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyContrastFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithTestPatternImages(nameof(ContrastValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyContrastFilterInBox(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(ContrastValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyContrastFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index 9053308680..59d888c14a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -11,11 +11,13 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class FilterTest : FileTestBase + public class FilterTest { + // Testing the generic FilterProcessor with more than one pixel type intentionally. + // There is no need to do this with the specialized ones. [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] + public void ApplyFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -29,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs index 23d514f352..9e8b9c0297 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs @@ -11,7 +11,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class GrayscaleTest : FileTestBase + public class GrayscaleTest { public static readonly TheoryData GrayscaleModeTypes = new TheoryData @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters /// Use test patterns over loaded images to save decode time. /// [Theory] - [WithTestPatternImages(nameof(GrayscaleModeTypes), 100, 100, DefaultPixelType)] - public void ImageShouldApplyGrayscaleFilter(TestImageProvider provider, GrayscaleMode value) + [WithTestPatternImages(nameof(GrayscaleModeTypes), 48, 48, PixelTypes.Rgba32)] + public void ApplyGrayscaleFilter(TestImageProvider provider, GrayscaleMode value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -45,8 +45,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(nameof(GrayscaleModeTypes), 100, 100, DefaultPixelType)] - public void ImageShouldApplyGrayscaleFilterInBox(TestImageProvider provider, GrayscaleMode value) + [WithTestPatternImages(nameof(GrayscaleModeTypes), 48, 48, PixelTypes.Rgba32)] + public void ApplyGrayscaleFilterInBox(TestImageProvider provider, GrayscaleMode value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs index 5a34595a62..317dce1c0b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class HueTest : FileTestBase + public class HueTest { public static readonly TheoryData HueValues = new TheoryData @@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters }; [Theory] - [WithTestPatternImages(nameof(HueValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyHueFilter(TestImageProvider provider, int value) + [WithTestPatternImages(nameof(HueValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyHueFilter(TestImageProvider provider, int value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(nameof(HueValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyHueFilterInBox(TestImageProvider provider, int value) + [WithTestPatternImages(nameof(HueValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyHueFilterInBox(TestImageProvider provider, int value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs index 2199e691fa..cac1e94ed3 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { - public class InvertTest : FileTestBase + public class InvertTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyInvertFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyInvertFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyInvertFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyInvertFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs index 6d95baaef0..1ae6fc8ad5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class KodachromeTest : FileTestBase + public class KodachromeTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyKodachromeFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyKodachromeFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyKodachromeFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyKodachromeFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 2f9cd4319b..ed16e3e0ee 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class LomographTest : FileTestBase + public class LomographTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyLomographFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyLomographFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyLomographFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyLomographFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs index 12bf93299a..4b1345bf0b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { - public class OpacityTest : FileTestBase + public class OpacityTest { public static readonly TheoryData AlphaValues = new TheoryData @@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects }; [Theory] - [WithTestPatternImages(nameof(AlphaValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyAlphaFilter(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(AlphaValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyAlphaFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects } [Theory] - [WithTestPatternImages(nameof(AlphaValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplyAlphaFilterInBox(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(AlphaValues), 48, 48, PixelTypes.Rgba32)] + public void ApplyAlphaFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs index 44e69c09ee..2d3cdf6d49 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class PolaroidTest : FileTestBase + public class PolaroidTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyPolaroidFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyPolaroidFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplyPolaroidFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplyPolaroidFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs index 8553cad4c2..a7fc332bde 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class SaturateTest : FileTestBase + public class SaturateTest { public static readonly TheoryData SaturationValues = new TheoryData @@ -19,8 +19,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters }; [Theory] - [WithTestPatternImages(nameof(SaturationValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplySaturationFilter(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(SaturationValues), 48, 48, PixelTypes.Rgba32)] + public void ApplySaturationFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -31,8 +31,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(nameof(SaturationValues), 100, 100, DefaultPixelType)] - public void ImageShouldApplySaturationFilterInBox(TestImageProvider provider, float value) + [WithTestPatternImages(nameof(SaturationValues), 48, 48, PixelTypes.Rgba32)] + public void ApplySaturationFilterInBox(TestImageProvider provider, float value) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs index 499567ae88..0cc4a0520e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs @@ -9,11 +9,11 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class SepiaTest : FileTestBase + public class SepiaTest { [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplySepiaFilter(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplySepiaFilter(TestImageProvider provider) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(100, 100, DefaultPixelType)] - public void ImageShouldApplySepiaFilterInBox(TestImageProvider provider) + [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] + public void ApplySepiaFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { using (Image source = provider.GetImage()) From af56af16dad98a568d8a227cbf9694f2750cfa8e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 24 Dec 2017 04:18:44 +0100 Subject: [PATCH 52/99] introducing [GroupOutput] + apply it to all filter tests --- .../Processors/Filters/BlackWhiteTest.cs | 1 + .../Processors/Filters/BrightnessTest.cs | 1 + .../Processors/Filters/ColorBlindnessTest.cs | 7 ++-- .../Processors/Filters/ContrastTest.cs | 1 + .../Processors/Filters/FilterTest.cs | 1 + .../Processors/Filters/GrayscaleTest.cs | 1 + .../Processing/Processors/Filters/HueTest.cs | 1 + .../Processors/Filters/InvertTest.cs | 1 + .../Processors/Filters/KodachromeTest.cs | 1 + .../Processors/Filters/LomographTest.cs | 1 + .../Processors/Filters/OpacityTest.cs | 1 + .../Processors/Filters/PolaroidTest.cs | 1 + .../Processors/Filters/SaturateTest.cs | 1 + .../Processors/Filters/SepiaTest.cs | 1 + .../Attributes/GroupOutputAttribute.cs | 17 ++++++++ .../ImageProviders/TestImageProvider.cs | 24 +++++++++--- .../TestUtilities/ImagingTestCaseUtility.cs | 39 ++++++------------- .../TestUtilities/Tests/GroupOutputTests.cs | 30 ++++++++++++++ .../Tests/TestImageProviderTests.cs | 8 ++++ 19 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 tests/ImageSharp.Tests/TestUtilities/Attributes/GroupOutputAttribute.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/Tests/GroupOutputTests.cs diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs index 2b9620ed5e..b0b9aaa492 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class BlackWhiteTest { [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index 00bc605478..eccf27899a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { + [GroupOutput("Filters")] public class BrightnessTest { public static readonly TheoryData BrightnessValues diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index 36f4a31407..6c51afd003 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -10,7 +10,8 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - public class ColorBlindnessTest : FileTestBase + [GroupOutput("Filters")] + public class ColorBlindnessTest { public static readonly TheoryData ColorBlindnessFilters = new TheoryData @@ -26,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters }; [Theory] - [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] + [WithTestPatternImages(nameof(ColorBlindnessFilters), 48, 48, PixelTypes.Rgba32)] public void ApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { @@ -38,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters } [Theory] - [WithTestPatternImages(nameof(ColorBlindnessFilters), 100, 100, DefaultPixelType)] + [WithTestPatternImages(nameof(ColorBlindnessFilters), 48, 48, PixelTypes.Rgba32)] public void ApplyColorBlindnessFilterInBox(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs index caa09c579e..337b810181 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { + [GroupOutput("Filters")] public class ContrastTest { public static readonly TheoryData ContrastValues diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index 59d888c14a..a98153087b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -11,6 +11,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class FilterTest { // Testing the generic FilterProcessor with more than one pixel type intentionally. diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs index 9e8b9c0297..711c8e10af 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs @@ -11,6 +11,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class GrayscaleTest { public static readonly TheoryData GrayscaleModeTypes diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs index 317dce1c0b..98dd95515d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class HueTest { public static readonly TheoryData HueValues diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs index cac1e94ed3..69df033f01 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { + [GroupOutput("Filters")] public class InvertTest { [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs index 1ae6fc8ad5..6daef29aac 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class KodachromeTest { [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index ed16e3e0ee..4e54828a67 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class LomographTest { [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs index 4b1345bf0b..9ba77b9836 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { + [GroupOutput("Filters")] public class OpacityTest { public static readonly TheoryData AlphaValues diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs index 2d3cdf6d49..42bd859dc6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class PolaroidTest { [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs index a7fc332bde..8cfbb198c8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class SaturateTest { public static readonly TheoryData SaturationValues diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs index 0cc4a0520e..9947d21d05 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs @@ -9,6 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + [GroupOutput("Filters")] public class SepiaTest { [Theory] diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/GroupOutputAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/GroupOutputAttribute.cs new file mode 100644 index 0000000000..b2967058c0 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/GroupOutputAttribute.cs @@ -0,0 +1,17 @@ +namespace SixLabors.ImageSharp.Tests +{ + using System; + + /// + /// The output produced by this test class should be grouped into the specified subfolder. + /// + public class GroupOutputAttribute : Attribute + { + public GroupOutputAttribute(string subfolder) + { + this.Subfolder = subfolder; + } + + public string Subfolder { get; } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index 91bbd32efe..1352a2476a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -11,7 +11,9 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests { - public interface ITestImageProvider + using Castle.Core.Internal; + + public interface ITestImageProvider { PixelTypes PixelType { get; } ImagingTestCaseUtility Utility { get; } @@ -34,6 +36,7 @@ namespace SixLabors.ImageSharp.Tests public string TypeName { get; private set; } public string MethodName { get; private set; } + public string OutputSubfolderName { get; private set; } public static TestImageProvider TestPattern( int width, @@ -101,8 +104,9 @@ namespace SixLabors.ImageSharp.Tests PixelTypes pixelType = info.GetValue("PixelType"); string typeName = info.GetValue("TypeName"); string methodName = info.GetValue("MethodName"); + string outputSubfolderName = info.GetValue("OutputSubfolderName"); - this.Init(typeName, methodName, pixelType); + this.Init(typeName, methodName, outputSubfolderName, pixelType); } public virtual void Serialize(IXunitSerializationInfo info) @@ -110,9 +114,14 @@ namespace SixLabors.ImageSharp.Tests info.AddValue("PixelType", this.PixelType); info.AddValue("TypeName", this.TypeName); info.AddValue("MethodName", this.MethodName); + info.AddValue("OutputSubfolderName", this.OutputSubfolderName); } - protected TestImageProvider Init(string typeName, string methodName, PixelTypes pixelTypeOverride) + protected TestImageProvider Init( + string typeName, + string methodName, + string outputSubfolerName, + PixelTypes pixelTypeOverride) { if (pixelTypeOverride != PixelTypes.Undefined) { @@ -120,7 +129,8 @@ namespace SixLabors.ImageSharp.Tests } this.TypeName = typeName; this.MethodName = methodName; - + this.OutputSubfolderName = outputSubfolerName; + this.Utility = new ImagingTestCaseUtility { SourceFileOrDescription = this.SourceFileOrDescription, @@ -129,7 +139,7 @@ namespace SixLabors.ImageSharp.Tests if (methodName != null) { - this.Utility.Init(typeName, methodName); + this.Utility.Init(typeName, methodName, outputSubfolerName); } return this; @@ -137,7 +147,9 @@ namespace SixLabors.ImageSharp.Tests protected TestImageProvider Init(MethodInfo testMethod, PixelTypes pixelTypeOverride) { - return Init(testMethod?.DeclaringType.Name, testMethod?.Name, pixelTypeOverride); + string subfolder = testMethod?.DeclaringType.GetAttribute()?.Subfolder + ?? string.Empty; + return this.Init(testMethod?.DeclaringType.Name, testMethod?.Name, subfolder, pixelTypeOverride); } public override string ToString() diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 7da0f0696d..e7dfe54881 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -34,6 +34,8 @@ namespace SixLabors.ImageSharp.Tests /// public string TestGroupName { get; set; } = string.Empty; + public string OutputSubfolderName { get; set; } = string.Empty; + /// /// The name of the test case (by default) /// @@ -165,41 +167,22 @@ namespace SixLabors.ImageSharp.Tests ); } - internal void Init(string typeName, string methodName) + internal void Init(string typeName, string methodName, string outputSubfolderName) { this.TestGroupName = typeName; this.TestName = methodName; + this.OutputSubfolderName = outputSubfolderName; } - - internal void Init(MethodInfo method) - { - this.Init(method.DeclaringType.Name, method.Name); - } - - //private static IImageEncoder GetEncoderByExtension(string extension, bool grayscale) - //{ - // extension = extension?.TrimStart('.'); - // var format = Configuration.Default.FindFormatByFileExtension(extension); - // IImageEncoder encoder = Configuration.Default.FindEncoder(format); - // PngEncoder pngEncoder = encoder as PngEncoder; - // if (pngEncoder != null) - // { - // pngEncoder = new PngEncoder(); - // encoder = pngEncoder; - // pngEncoder.CompressionLevel = 9; - - // if (grayscale) - // { - // pngEncoder.PngColorType = PngColorType.Grayscale; - // } - // } - - // return encoder; - //} - + internal string GetTestOutputDir() { string testGroupName = Path.GetFileNameWithoutExtension(this.TestGroupName); + + if (!string.IsNullOrEmpty(this.OutputSubfolderName)) + { + testGroupName = Path.Combine(this.OutputSubfolderName, testGroupName); + } + return TestEnvironment.CreateOutputDirectory(testGroupName); } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/GroupOutputTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/GroupOutputTests.cs new file mode 100644 index 0000000000..be12678c88 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/GroupOutputTests.cs @@ -0,0 +1,30 @@ +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests +{ + using System.IO; + + using SixLabors.ImageSharp.PixelFormats; + + using Xunit; + + [GroupOutput("Foo")] + public class GroupOutputTests + { + [Theory] + [WithBlankImages(1, 1, PixelTypes.Rgba32)] + public void OutputSubfolderName_ValueIsTakeFromGroupOutputAttribute(TestImageProvider provider) + where TPixel : struct, IPixel + { + Assert.Equal("Foo", provider.Utility.OutputSubfolderName); + } + + [Theory] + [WithBlankImages(1,1, PixelTypes.Rgba32)] + public void GetTestOutputDir_ShouldDefineSubfolder(TestImageProvider provider) + where TPixel : struct, IPixel + { + string expected = $"{Path.DirectorySeparatorChar}Foo{Path.DirectorySeparatorChar}"; + Assert.Contains(expected, provider.Utility.GetTestOutputDir()); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index e3249fae9f..f0adeb7534 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -22,6 +22,14 @@ namespace SixLabors.ImageSharp.Tests private ITestOutputHelper Output { get; } + [Theory] + [WithBlankImages(1, 1, PixelTypes.Rgba32)] + public void NoOutputSubfolderIsPresentByDefault(TestImageProvider provider) + where TPixel : struct, IPixel + { + Assert.Empty(provider.Utility.OutputSubfolderName); + } + [Theory] [WithBlankImages(42, 666, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.HalfSingle, "hello")] public void Use_WithEmptyImageAttribute(TestImageProvider provider, string message) From 5041e5f54218573f20c215e148e22e9e587f7b2e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 24 Dec 2017 04:33:20 +0100 Subject: [PATCH 53/99] fixing StyleCop issue --- .../Processing/Processors/Filters/BlackWhiteProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs index 141dc493a7..30fcfab4fd 100644 --- a/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/BlackWhiteProcessor.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Applies a black and white filter matrix to the image /// /// The pixel format. - internal class BlackWhiteProcessor : FilterProcessor + internal class BlackWhiteProcessor : FilterProcessor where TPixel : struct, IPixel { /// From 81743c761eb7ee5161db64496e50ecc016e0fa21 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 24 Dec 2017 16:42:27 +1100 Subject: [PATCH 54/99] Make TransformHelpers internal --- src/ImageSharp/Processing/Transforms/TransformHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs index 6f9560a9fa..419c1c13d6 100644 --- a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp /// /// Contains helper methods for working with affine transforms /// - public class TransformHelpers + internal class TransformHelpers { /// /// Returns the bounding relative to the source for the given transformation matrix. From 34de0b62c91322ba96289ea7f8841474a2cee905 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 25 Dec 2017 00:26:12 +0100 Subject: [PATCH 55/99] yet another test case for TransformTests --- tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index ac1e3812dc..d5e1f144a1 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -137,6 +137,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { 0, 0, 10, 10 }, { 0, 0, 5, 10 }, { 0, 0, 10, 5 }, + { 5, 0, 5, 10 }, {-5,-5, 15, 15 } }; From 48ffa1fd81e390dc391f0621ed252fc70b450f6a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 15 Jan 2018 23:29:43 +0100 Subject: [PATCH 56/99] PixelDataPool: reduce maximum pooled array size to 32MB for all value types --- src/ImageSharp/Memory/PixelDataPool{T}.cs | 34 ++++----- .../Memory/PixelDataPoolTests.cs | 70 +++++++++++++++---- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Memory/PixelDataPool{T}.cs b/src/ImageSharp/Memory/PixelDataPool{T}.cs index 6f4cef707a..f25803951c 100644 --- a/src/ImageSharp/Memory/PixelDataPool{T}.cs +++ b/src/ImageSharp/Memory/PixelDataPool{T}.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Buffers; -using SixLabors.ImageSharp.PixelFormats; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { @@ -13,10 +13,21 @@ namespace SixLabors.ImageSharp.Memory internal class PixelDataPool where T : struct { + /// + /// The maximum size of pooled arrays in bytes. + /// Currently set to 32MB, which is equivalent to 8 megapixels of raw data. + /// + private const int MaxPooledBufferSizeInBytes = 32 * 1024 * 1024; + + /// + /// The maximum array length of the . + /// + private static readonly int MaxArrayLength = MaxPooledBufferSizeInBytes / Unsafe.SizeOf(); + /// /// The which is not kept clean. /// - private static readonly ArrayPool ArrayPool = ArrayPool.Create(CalculateMaxArrayLength(), 50); + private static readonly ArrayPool ArrayPool = ArrayPool.Create(MaxArrayLength, 50); /// /// Rents the pixel array from the pool. @@ -36,24 +47,5 @@ namespace SixLabors.ImageSharp.Memory { ArrayPool.Return(array); } - - /// - /// Heuristically calculates a reasonable maxArrayLength value for the backing . - /// - /// The maxArrayLength value - internal static int CalculateMaxArrayLength() - { - // ReSharper disable once SuspiciousTypeConversion.Global - if (default(T) is IPixel) - { - const int MaximumExpectedImageSize = 16384 * 16384; - return MaximumExpectedImageSize; - } - else - { - const int MaxArrayLength = 1024 * 1024; // Match default pool. - return MaxArrayLength; - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs b/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs index fdfd4c4b7f..beaa49dbc2 100644 --- a/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs +++ b/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs @@ -1,20 +1,21 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. - +using System.Linq; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Memory { - using SixLabors.ImageSharp.Memory; - - using Xunit; - /// /// Tests the class. /// public class PixelDataPoolTests { + readonly object monitor = new object(); + [Fact] public void PixelDataPoolRentsMinimumSize() { @@ -33,23 +34,62 @@ namespace SixLabors.ImageSharp.Tests.Memory Assert.True(pixels.Length >= 1024); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void CalculateMaxArrayLength(bool isRawData) + /// + /// Rent 'n' buffers -> return all -> re-rent, verify if there is at least one in common. + /// + private bool CheckIsPooled(int n, int count) + where T : struct + { + lock (this.monitor) + { + T[][] original = new T[n][]; + + for (int i = 0; i < n; i++) + { + original[i] = PixelDataPool.Rent(count); + } + + for (int i = 0; i < n; i++) + { + PixelDataPool.Return(original[i]); + } + + T[][] verification = new T[n][]; + + for (int i = 0; i < n; i++) + { + verification[i] = PixelDataPool.Rent(count); + } + + return original.Intersect(verification).Any(); + } + } + + [Fact] + public void SmallBuffersArePooled() { - int max = isRawData ? PixelDataPool.CalculateMaxArrayLength() - : PixelDataPool.CalculateMaxArrayLength(); + Assert.True(this.CheckIsPooled(5, 512)); + } - Assert.Equal(max > 1024 * 1024, !isRawData); + [Fact] + public void LargeBuffersAreNotPooled_OfByte() + { + const int mb128 = 128 * 1024 * 1024; + Assert.False(this.CheckIsPooled(2, mb128)); + } + + [StructLayout(LayoutKind.Explicit, Size = 512)] + struct TestStruct + { } [Fact] - public void RentNonIPixelData() + public unsafe void LaregeBuffersAreNotPooled_OfBigValueType() { - byte[] data = PixelDataPool.Rent(16384); + const int mb128 = 128 * 1024 * 1024; + int count = mb128 / sizeof(TestStruct); - Assert.True(data.Length >= 16384); + Assert.False(this.CheckIsPooled(2, count)); } } } \ No newline at end of file From 8c231f713f3c35c823fdcb0d5a1b1e715537f3a3 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Tue, 16 Jan 2018 19:04:56 +0000 Subject: [PATCH 57/99] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..2eeb57968e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Six Labors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 5c68f7e42add58e864e121e2c78026bcd55443a7 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Tue, 16 Jan 2018 19:05:41 +0000 Subject: [PATCH 58/99] Delete APACHE-2.0-LICENSE.txt --- APACHE-2.0-LICENSE.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 APACHE-2.0-LICENSE.txt diff --git a/APACHE-2.0-LICENSE.txt b/APACHE-2.0-LICENSE.txt deleted file mode 100644 index a666c6e078..0000000000 --- a/APACHE-2.0-LICENSE.txt +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2012 James South - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file From a631a64dfb5a36e2a2087d7beac75c7be870a554 Mon Sep 17 00:00:00 2001 From: denisivan0v Date: Wed, 17 Jan 2018 16:55:53 +0700 Subject: [PATCH 59/99] Added an API to read base image information without decoding it - intoduced base IImage interface - introduced IImageInfoDetector interface - Image.DetectPixelType method expanded to Identify method that returns IImage --- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 11 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 134 +++++++------ src/ImageSharp/Formats/Gif/GifDecoder.cs | 17 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 182 +++++++++++++----- .../Sections/GifLogicalScreenDescriptor.cs | 5 + src/ImageSharp/Formats/IImageDecoder.cs | 10 - src/ImageSharp/Formats/IImageInfoDetector.cs | 21 ++ .../Jpeg/GolangPort/OrigJpegDecoder.cs | 6 +- .../Jpeg/GolangPort/OrigJpegDecoderCore.cs | 14 +- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 6 +- .../Jpeg/PdfJsPort/PdfJsJpegDecoder.cs | 6 - .../Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs | 2 +- src/ImageSharp/Formats/PixelTypeInfo.cs | 6 +- src/ImageSharp/Formats/Png/PngDecoder.cs | 15 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 69 ++++--- src/ImageSharp/Image/IImage.cs | 31 +++ src/ImageSharp/Image/Image.Decode.cs | 10 +- src/ImageSharp/Image/Image.FromStream.cs | 16 +- src/ImageSharp/Image/ImageFrameCollection.cs | 4 +- src/ImageSharp/Image/ImageInfo.cs | 24 +++ src/ImageSharp/Image/Image{TPixel}.cs | 31 +-- .../Processors/Transforms/ResizeProcessor.cs | 2 +- .../Formats/Bmp/BmpDecoderTests.cs | 2 +- .../Formats/Gif/GifDecoderTests.cs | 2 +- .../Formats/Jpg/JpegDecoderTests.cs | 2 +- .../Formats/Png/PngDecoderTests.cs | 2 +- tests/ImageSharp.Tests/TestFormat.cs | 5 - .../SystemDrawingReferenceDecoder.cs | 21 +- .../Tests/TestImageProviderTests.cs | 10 - 29 files changed, 418 insertions(+), 248 deletions(-) create mode 100644 src/ImageSharp/Formats/IImageInfoDetector.cs create mode 100644 src/ImageSharp/Image/IImage.cs create mode 100644 src/ImageSharp/Image/ImageInfo.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index b976d66797..e252e63406 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.PixelFormats; @@ -23,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Formats will be supported in a later releases. We advise always /// to use only 24 Bit Windows bitmaps. /// - public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions + public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions, IImageInfoDetector { /// public Image Decode(Configuration configuration, Stream stream) @@ -36,14 +34,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + public IImage Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); - byte[] buffer = new byte[2]; - stream.Skip(28); - stream.Read(buffer, 0, 2); - return new PixelTypeInfo(BitConverter.ToInt16(buffer, 0)); + return new BmpDecoderCore(configuration, this).Identify(stream); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 04176e0333..ef9e761643 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -94,62 +95,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp public Image Decode(Stream stream) where TPixel : struct, IPixel { - this.currentStream = stream; - try { - this.ReadFileHeader(); - this.ReadInfoHeader(); - - // see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517 - // If the height is negative, then this is a Windows bitmap whose origin - // is the upper-left corner and not the lower-left.The inverted flag - // indicates a lower-left origin.Our code will be outputting an - // upper-left origin pixel array. - bool inverted = false; - if (this.infoHeader.Height < 0) - { - inverted = true; - this.infoHeader.Height = -this.infoHeader.Height; - } - - int colorMapSize = -1; - - if (this.infoHeader.ClrUsed == 0) - { - if (this.infoHeader.BitsPerPixel == 1 || - this.infoHeader.BitsPerPixel == 4 || - this.infoHeader.BitsPerPixel == 8) - { - colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; - } - } - else - { - colorMapSize = this.infoHeader.ClrUsed * 4; - } - - byte[] palette = null; - - if (colorMapSize > 0) - { - // 256 * 4 - if (colorMapSize > 1024) - { - throw new ImageFormatException($"Invalid bmp colormap size '{colorMapSize}'"); - } - - palette = new byte[colorMapSize]; - - this.currentStream.Read(palette, 0, colorMapSize); - } - - if (this.infoHeader.Width > int.MaxValue || this.infoHeader.Height > int.MaxValue) - { - throw new ArgumentOutOfRangeException( - $"The input bitmap '{this.infoHeader.Width}x{this.infoHeader.Height}' is " - + $"bigger then the max allowed size '{int.MaxValue}x{int.MaxValue}'"); - } + this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); var image = new Image(this.configuration, this.infoHeader.Width, this.infoHeader.Height); using (PixelAccessor pixels = image.Lock()) @@ -192,6 +140,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } + /// + /// Reads the image base information from the specified stream. + /// + /// The containing image data. + public IImage Identify(Stream stream) + { + this.ReadImageHeaders(stream, out _, out _); + return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, new ImageMetaData()); + } + /// /// Returns the y- value based on the given height. /// @@ -624,5 +582,73 @@ namespace SixLabors.ImageSharp.Formats.Bmp Offset = BitConverter.ToInt32(data, 10) }; } + + /// + /// Reads the and from the stream and sets the corresponding fields. + /// + private void ReadImageHeaders(Stream stream, out bool inverted, out byte[] palette) + { + this.currentStream = stream; + + try + { + this.ReadFileHeader(); + this.ReadInfoHeader(); + + // see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517 + // If the height is negative, then this is a Windows bitmap whose origin + // is the upper-left corner and not the lower-left.The inverted flag + // indicates a lower-left origin.Our code will be outputting an + // upper-left origin pixel array. + inverted = false; + if (this.infoHeader.Height < 0) + { + inverted = true; + this.infoHeader.Height = -this.infoHeader.Height; + } + + int colorMapSize = -1; + + if (this.infoHeader.ClrUsed == 0) + { + if (this.infoHeader.BitsPerPixel == 1 || + this.infoHeader.BitsPerPixel == 4 || + this.infoHeader.BitsPerPixel == 8) + { + colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; + } + } + else + { + colorMapSize = this.infoHeader.ClrUsed * 4; + } + + palette = null; + + if (colorMapSize > 0) + { + // 256 * 4 + if (colorMapSize > 1024) + { + throw new ImageFormatException($"Invalid bmp colormap size '{colorMapSize}'"); + } + + palette = new byte[colorMapSize]; + + this.currentStream.Read(palette, 0, colorMapSize); + } + + if (this.infoHeader.Width > int.MaxValue || this.infoHeader.Height > int.MaxValue) + { + throw new ArgumentOutOfRangeException( + $"The input bitmap '{this.infoHeader.Width}x{this.infoHeader.Height}' is " + + $"bigger then the max allowed size '{int.MaxValue}x{int.MaxValue}'"); + } + } + catch (IndexOutOfRangeException e) + { + throw new ImageFormatException("Bitmap does not have a valid format.", e); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 20bebb1d34..ccb6cf92c5 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; using System.IO; using System.Text; using SixLabors.ImageSharp.PixelFormats; @@ -12,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Decoder for generating an image out of a gif encoded stream. /// - public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions + public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDetector { /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -33,20 +31,17 @@ namespace SixLabors.ImageSharp.Formats.Gif public Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel { - var decoder = new GifDecoderCore(configuration, this); - return decoder.Decode(stream); + var decoder = new GifDecoderCore(configuration, this); + return decoder.Decode(stream); } /// - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + public IImage Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); - byte[] buffer = new byte[1]; - stream.Skip(10); // Skip the identifier and size - stream.Read(buffer, 0, 1); // Skip the identifier and size - int bitsPerPixel = (buffer[0] & 0x07) + 1; // The lowest 3 bits represent the bit depth minus 1 - return new PixelTypeInfo(bitsPerPixel); + var decoder = new GifDecoderCore(configuration, this); + return decoder.Identify(stream); } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index ae20be7d5d..dbca49f068 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -17,9 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Performs the gif decoding operation. /// - /// The pixel format. - internal sealed class GifDecoderCore - where TPixel : struct, IPixel + internal sealed class GifDecoderCore { /// /// The temp buffer used to reduce allocations. @@ -46,11 +44,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private int globalColorTableLength; - /// - /// The previous frame. - /// - private ImageFrame previousFrame; - /// /// The area to restore. /// @@ -72,12 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private ImageMetaData metaData; /// - /// The image to decode the information to. - /// - private Image image; - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. /// The decoder options. @@ -107,28 +95,84 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Decodes the stream to the image. /// + /// The pixel format. /// The stream containing image data. /// The decoded image - public Image Decode(Stream stream) + public Image Decode(Stream stream) + where TPixel : struct, IPixel { + Image image = null; + ImageFrame previousFrame = null; try { - this.metaData = new ImageMetaData(); + this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); - this.currentStream = stream; + // Loop though the respective gif parts and read the data. + int nextFlag = stream.ReadByte(); + while (nextFlag != GifConstants.Terminator) + { + if (nextFlag == GifConstants.ImageLabel) + { + if (previousFrame != null && this.DecodingMode == FrameDecodingMode.First) + { + break; + } - // Skip the identifier - this.currentStream.Skip(6); - this.ReadLogicalScreenDescriptor(); + this.ReadFrame(ref image, ref previousFrame); + } + else if (nextFlag == GifConstants.ExtensionIntroducer) + { + int label = stream.ReadByte(); + switch (label) + { + case GifConstants.GraphicControlLabel: + this.ReadGraphicalControlExtension(); + break; + case GifConstants.CommentLabel: + this.ReadComments(); + break; + case GifConstants.ApplicationExtensionLabel: - if (this.logicalScreenDescriptor.GlobalColorTableFlag) - { - this.globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; - this.globalColorTable = Buffer.CreateClean(this.globalColorTableLength); + // The application extension length should be 11 but we've got test images that incorrectly + // set this to 252. + int appLength = stream.ReadByte(); + this.Skip(appLength); // No need to read. + break; + case GifConstants.PlainTextLabel: + int plainLength = stream.ReadByte(); + this.Skip(plainLength); // Not supported by any known decoder. + break; + } + } + else if (nextFlag == GifConstants.EndIntroducer) + { + break; + } - // Read the global color table from the stream - stream.Read(this.globalColorTable.Array, 0, this.globalColorTableLength); + nextFlag = stream.ReadByte(); + if (nextFlag == -1) + { + break; + } } + } + finally + { + this.globalColorTable?.Dispose(); + } + + return image; + } + + /// + /// Reads the image base information from the specified stream. + /// + /// The containing image data. + public IImage Identify(Stream stream) + { + try + { + this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); // Loop though the respective gif parts and read the data. int nextFlag = stream.ReadByte(); @@ -136,12 +180,19 @@ namespace SixLabors.ImageSharp.Formats.Gif { if (nextFlag == GifConstants.ImageLabel) { - if (this.previousFrame != null && this.DecodingMode == FrameDecodingMode.First) + GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); + + // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. + if (imageDescriptor.LocalColorTableFlag) { - break; + int length = imageDescriptor.LocalColorTableSize * 3; + + // Skip local color table block + this.Skip(length); } - this.ReadFrame(); + // Skip image block + this.Skip(0); } else if (nextFlag == GifConstants.ExtensionIntroducer) { @@ -149,7 +200,9 @@ namespace SixLabors.ImageSharp.Formats.Gif switch (label) { case GifConstants.GraphicControlLabel: - this.ReadGraphicalControlExtension(); + + // Skip graphic control extension block + this.Skip(0); break; case GifConstants.CommentLabel: this.ReadComments(); @@ -184,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - return this.image; + return new ImageInfo(new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height, this.metaData); } /// @@ -242,6 +295,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { Width = BitConverter.ToInt16(this.buffer, 0), Height = BitConverter.ToInt16(this.buffer, 2), + BitsPerPixel = (this.buffer[4] & 0x07) + 1, // The lowest 3 bits represent the bit depth minus 1 BackgroundColorIndex = this.buffer[5], PixelAspectRatio = this.buffer[6], GlobalColorTableFlag = ((packed & 0x80) >> 7) == 1, @@ -308,7 +362,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Reads an individual gif frame. /// - private void ReadFrame() + /// The pixel format. + /// The image to decode the information to. + /// The previous frame. + private void ReadFrame(ref Image image, ref ImageFrame previousFrame) + where TPixel : struct, IPixel { GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); @@ -327,7 +385,7 @@ namespace SixLabors.ImageSharp.Formats.Gif indices = Buffer.CreateClean(imageDescriptor.Width * imageDescriptor.Height); this.ReadFrameIndices(imageDescriptor, indices); - this.ReadFrameColors(indices, localColorTable ?? this.globalColorTable, imageDescriptor); + this.ReadFrameColors(ref image, ref previousFrame, indices, localColorTable ?? this.globalColorTable, imageDescriptor); // Skip any remaining blocks this.Skip(0); @@ -357,10 +415,14 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Reads the frames colors, mapping indices to colors. /// + /// The pixel format. + /// The image to decode the information to. + /// The previous frame. /// The indexed pixels. /// The color table containing the available colors. /// The - private void ReadFrameColors(Span indices, Span colorTable, GifImageDescriptor descriptor) + private void ReadFrameColors(ref Image image, ref ImageFrame previousFrame, Span indices, Span colorTable, GifImageDescriptor descriptor) + where TPixel : struct, IPixel { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; @@ -371,30 +433,30 @@ namespace SixLabors.ImageSharp.Formats.Gif ImageFrame imageFrame; - if (this.previousFrame == null) + if (previousFrame == null) { // This initializes the image to become fully transparent because the alpha channel is zero. - this.image = new Image(this.configuration, imageWidth, imageHeight, this.metaData); + image = new Image(this.configuration, new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), imageWidth, imageHeight, this.metaData); - this.SetFrameMetaData(this.image.Frames.RootFrame.MetaData); + this.SetFrameMetaData(image.Frames.RootFrame.MetaData); - imageFrame = this.image.Frames.RootFrame; + imageFrame = image.Frames.RootFrame; } else { if (this.graphicsControlExtension != null && this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { - prevFrame = this.previousFrame; + prevFrame = previousFrame; } - currentFrame = this.image.Frames.AddFrame(this.previousFrame); // This clones the frame and adds it the collection + currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection this.SetFrameMetaData(currentFrame.MetaData); imageFrame = currentFrame; - this.RestoreToBackground(imageFrame); + this.RestoreToBackground(imageFrame, image.Width, image.Height); } int i = 0; @@ -466,11 +528,11 @@ namespace SixLabors.ImageSharp.Formats.Gif if (prevFrame != null) { - this.previousFrame = prevFrame; + previousFrame = prevFrame; return; } - this.previousFrame = currentFrame ?? this.image.Frames.RootFrame; + previousFrame = currentFrame ?? image.Frames.RootFrame; if (this.graphicsControlExtension != null && this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) @@ -482,8 +544,12 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Restores the current frame area to the background. /// + /// The pixel format. /// The frame. - private void RestoreToBackground(ImageFrame frame) + /// Width of the image. + /// Height of the image. + private void RestoreToBackground(ImageFrame frame, int imageWidth, int imageHeight) + where TPixel : struct, IPixel { if (this.restoreArea == null) { @@ -491,8 +557,8 @@ namespace SixLabors.ImageSharp.Formats.Gif } // Optimization for when the size of the frame is the same as the image size. - if (this.restoreArea.Value.Width == this.image.Width && - this.restoreArea.Value.Height == this.image.Height) + if (this.restoreArea.Value.Width == imageWidth && + this.restoreArea.Value.Height == imageHeight) { using (PixelAccessor pixelAccessor = frame.Lock()) { @@ -533,5 +599,29 @@ namespace SixLabors.ImageSharp.Formats.Gif meta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; } } + + /// + /// Reads the logical screen descriptor and global color table blocks + /// + /// The stream containing image data. + private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream) + { + this.metaData = new ImageMetaData(); + + this.currentStream = stream; + + // Skip the identifier + this.currentStream.Skip(6); + this.ReadLogicalScreenDescriptor(); + + if (this.logicalScreenDescriptor.GlobalColorTableFlag) + { + this.globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; + this.globalColorTable = Buffer.CreateClean(this.globalColorTableLength); + + // Read the global color table from the stream + stream.Read(this.globalColorTable.Array, 0, this.globalColorTableLength); + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs b/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs index b1109c3e25..9fea5b1126 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs @@ -21,6 +21,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// rendered in the displaying device. /// public short Height { get; set; } + + /// + /// Gets or sets the color depth, in number of bits per pixel. + /// + public int BitsPerPixel { get; set; } /// /// Gets or sets the index at the Global Color Table for the Background Color. diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index 0d16c7db22..ffc40314d8 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.PixelFormats; @@ -22,13 +20,5 @@ namespace SixLabors.ImageSharp.Formats /// The decoded image Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel; - - /// - /// Detects the image pixel size from the specified stream. - /// - /// The configuration for the image. - /// The containing image data. - /// The object - PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream); } } diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs new file mode 100644 index 0000000000..4e1dfc84f6 --- /dev/null +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +namespace SixLabors.ImageSharp.Formats +{ + /// + /// Used for detecting the image base information without decoding it. + /// + public interface IImageInfoDetector + { + /// + /// Reads the image base information from the specified stream. + /// + /// The configuration for the image. + /// The containing image data. + /// The object + IImage Identify(Configuration configuration, Stream stream); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs index 97a424c415..7b082a3d6a 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// /// Image decoder for generating an image out of a jpg stream. /// - internal sealed class OrigJpegDecoder : IImageDecoder, IJpegDecoderOptions + internal sealed class OrigJpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfoDetector { /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort } /// - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + public IImage Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); using (var decoder = new OrigJpegDecoderCore(configuration, this)) { - return new PixelTypeInfo(decoder.DetectPixelSize(stream)); + return decoder.Identify(stream); } } } diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index 92e8557286..b7cad3281e 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -127,6 +127,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort IEnumerable IRawJpegData.Components => this.Components; + /// + /// Gets the color depth, in number of bits per pixel. + /// + public int BitsPerPixel => this.ComponentCount * SupportedPrecision; + /// /// Gets the image height /// @@ -193,14 +198,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort } /// - /// Detects the image pixel size from the specified stream. + /// Reads the image base information from the specified stream. /// /// The containing image data. - /// The color depth, in number of bits per pixel - public int DetectPixelSize(Stream stream) + public IImage Identify(Stream stream) { this.ParseStream(stream, true); - return this.ComponentCount * SupportedPrecision; + return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); } /// @@ -785,7 +789,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { using (var postProcessor = new JpegImagePostProcessor(this)) { - var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, this.MetaData); + var image = new Image(this.configuration, new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); postProcessor.PostProcess(image.Frames.RootFrame); return image; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 6845314c1c..ee7d7e6996 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Image decoder for generating an image out of a jpg stream. /// - public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions + public sealed class JpegDecoder : IImageDecoder, IJpegDecoderOptions, IImageInfoDetector { /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -31,13 +31,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + public IImage Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); using (var decoder = new OrigJpegDecoderCore(configuration, this)) { - return new PixelTypeInfo(decoder.DetectPixelSize(stream)); + return decoder.Identify(stream); } } } diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs index 1458d54e5f..37ce0151f3 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoder.cs @@ -27,11 +27,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort return decoder.Decode(stream); } } - - /// - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) - { - throw new System.NotImplementedException(); - } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs index 211c24d208..c93adac2d4 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs @@ -159,7 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort this.QuantizeAndInverseAllComponents(); - var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, metadata); + var image = new Image(this.configuration, null, this.ImageWidth, this.ImageHeight, metadata); this.FillPixelData(image.Frames.RootFrame); this.AssignResolution(image); return image; diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index d4b72a00b4..a2a2ce9ce2 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -1,21 +1,21 @@ namespace SixLabors.ImageSharp.Formats { /// - /// Stores information about pixel + /// Stores information about pixel. /// public class PixelTypeInfo { /// /// Initializes a new instance of the class. /// - /// color depth, in number of bits per pixel + /// Color depth, in number of bits per pixel. internal PixelTypeInfo(int bitsPerPixel) { this.BitsPerPixel = bitsPerPixel; } /// - /// Gets color depth, in number of bits per pixel + /// Gets color depth, in number of bits per pixel. /// public int BitsPerPixel { get; } } diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index abb41770f9..8e110732ee 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; using System.IO; using System.Text; using SixLabors.ImageSharp.PixelFormats; @@ -29,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// /// - public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions + public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDetector { /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -55,16 +53,11 @@ namespace SixLabors.ImageSharp.Formats.Png return decoder.Decode(stream); } - /// - /// Detects the image pixel size from the specified stream. - /// - /// The configuration for the image. - /// The containing image data. - /// The color depth, in number of bits per pixel - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + /// + public IImage Identify(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - return new PixelTypeInfo(decoder.DetectPixelSize(stream)); + return decoder.Identify(stream); } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 52b571761b..e4c554437e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -237,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Png deframeStream.AllocateNewBytes(currentChunk.Length); this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame); - stream.Read(this.crcBuffer, 0, 4); + this.currentStream.Read(this.crcBuffer, 0, 4); break; case PngChunkTypes.Palette: byte[] pal = new byte[currentChunk.Length]; @@ -279,42 +279,50 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - /// Detects the image pixel size from the specified stream. + /// Reads the image base information from the specified stream. /// /// The containing image data. - /// The color depth, in number of bits per pixel - public int DetectPixelSize(Stream stream) + public IImage Identify(Stream stream) { + var metadata = new ImageMetaData(); this.currentStream = stream; this.currentStream.Skip(8); try { - PngChunk currentChunk; - while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null) + PngChunk currentChunk; + while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null) + { + try { - try + switch (currentChunk.Type) { - switch (currentChunk.Type) - { - case PngChunkTypes.Header: - this.ReadHeaderChunk(currentChunk.Data); - this.ValidateHeader(); - this.isEndChunkReached = true; - break; - case PngChunkTypes.End: - this.isEndChunkReached = true; - break; - } + case PngChunkTypes.Header: + this.ReadHeaderChunk(currentChunk.Data); + this.ValidateHeader(); + break; + case PngChunkTypes.Physical: + this.ReadPhysicalChunk(metadata, currentChunk.Data); + break; + case PngChunkTypes.Data: + this.SkipChunkDataAndCrc(currentChunk); + break; + case PngChunkTypes.Text: + this.ReadTextChunk(metadata, currentChunk.Data, currentChunk.Length); + break; + case PngChunkTypes.End: + this.isEndChunkReached = true; + break; } - finally + } + finally + { + // Data is rented in ReadChunkData() + if (currentChunk.Data != null) { - // Data is rented in ReadChunkData() - if (currentChunk.Data != null) - { - ArrayPool.Shared.Return(currentChunk.Data); - } + ArrayPool.Shared.Return(currentChunk.Data); } } + } } finally { @@ -327,7 +335,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("PNG Image hasn't header chunk"); } - return this.CalculateBitsPerPixel(); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); } /// @@ -418,7 +426,7 @@ namespace SixLabors.ImageSharp.Formats.Png private void InitializeImage(ImageMetaData metadata, out Image image) where TPixel : struct, IPixel { - image = new Image(this.configuration, this.header.Width, this.header.Height, metadata); + image = new Image(this.configuration, new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; @@ -1255,6 +1263,15 @@ namespace SixLabors.ImageSharp.Formats.Png } } + /// + /// Skips the chunk data and the cycle redundancy chunk read from the data. + /// + private void SkipChunkDataAndCrc(PngChunk chunk) + { + this.currentStream.Skip(chunk.Length); + this.currentStream.Skip(4); + } + /// /// Reads the chunk data from the stream. /// diff --git a/src/ImageSharp/Image/IImage.cs b/src/ImageSharp/Image/IImage.cs new file mode 100644 index 0000000000..f840c78f00 --- /dev/null +++ b/src/ImageSharp/Image/IImage.cs @@ -0,0 +1,31 @@ +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp +{ + /// + /// Represents the base image abstraction. + /// + public interface IImage + { + /// + /// Gets information about pixel. + /// + PixelTypeInfo PixelType { get; } + + /// + /// Gets the width. + /// + int Width { get; } + + /// + /// Gets the height. + /// + int Height { get; } + + /// + /// Gets the meta data of the image. + /// + ImageMetaData MetaData { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Image/Image.Decode.cs b/src/ImageSharp/Image/Image.Decode.cs index da2e036057..8f379cb109 100644 --- a/src/ImageSharp/Image/Image.Decode.cs +++ b/src/ImageSharp/Image/Image.Decode.cs @@ -81,17 +81,17 @@ namespace SixLabors.ImageSharp } /// - /// Detects the image pixel size. + /// Reads the image base information. /// /// The stream. /// the configuration. /// - /// The or null if suitable decoder not found. + /// The or null if suitable info detector not found. /// - private static PixelTypeInfo InternalDetectPixelType(Stream stream, Configuration config) + private static IImage InternalIdentity(Stream stream, Configuration config) { - IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat _); - return decoder?.DetectPixelType(config, stream); + var detector = DiscoverDecoder(stream, config, out IImageFormat _) as IImageInfoDetector; + return detector?.Identify(config, stream); } } } \ No newline at end of file diff --git a/src/ImageSharp/Image/Image.FromStream.cs b/src/ImageSharp/Image/Image.FromStream.cs index 2e0832b81f..0233efb208 100644 --- a/src/ImageSharp/Image/Image.FromStream.cs +++ b/src/ImageSharp/Image/Image.FromStream.cs @@ -37,22 +37,22 @@ namespace SixLabors.ImageSharp } /// - /// By reading the header on the provided stream this calculates the images color depth. + /// By reading the header on the provided stream this reads the image base information. /// /// The image stream to read the header from. /// /// Thrown if the stream is not readable nor seekable. /// /// - /// The or null if suitable decoder not found. + /// The or null if suitable info detector not found. /// - public static PixelTypeInfo DetectPixelType(Stream stream) + public static IImage Identify(Stream stream) { - return DetectPixelType(null, stream); + return Identify(null, stream); } /// - /// By reading the header on the provided stream this calculates the images color depth. + /// By reading the header on the provided stream this reads the image base information. /// /// The configuration. /// The image stream to read the header from. @@ -60,11 +60,11 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is not readable nor seekable. /// /// - /// The or null if suitable decoder not found. + /// The or null if suitable info detector not found. /// - public static PixelTypeInfo DetectPixelType(Configuration config, Stream stream) + public static IImage Identify(Configuration config, Stream stream) { - return WithSeekableStream(stream, s => InternalDetectPixelType(s, config ?? Configuration.Default)); + return WithSeekableStream(stream, s => InternalIdentity(s, config ?? Configuration.Default)); } /// diff --git a/src/ImageSharp/Image/ImageFrameCollection.cs b/src/ImageSharp/Image/ImageFrameCollection.cs index 3e9bb03435..ececcea895 100644 --- a/src/ImageSharp/Image/ImageFrameCollection.cs +++ b/src/ImageSharp/Image/ImageFrameCollection.cs @@ -129,7 +129,7 @@ namespace SixLabors.ImageSharp this.frames.Remove(frame); - return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame }); + return new Image(this.parent.GetConfiguration(), this.parent.PixelType, this.parent.MetaData.Clone(), new[] { frame }); } /// @@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp { ImageFrame frame = this[index]; ImageFrame clonedFrame = frame.Clone(); - return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame }); + return new Image(this.parent.GetConfiguration(), this.parent.PixelType, this.parent.MetaData.Clone(), new[] { clonedFrame }); } /// diff --git a/src/ImageSharp/Image/ImageInfo.cs b/src/ImageSharp/Image/ImageInfo.cs new file mode 100644 index 0000000000..11dcc3e752 --- /dev/null +++ b/src/ImageSharp/Image/ImageInfo.cs @@ -0,0 +1,24 @@ +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp +{ + internal sealed class ImageInfo : IImage + { + public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetaData metaData) + { + this.PixelType = pixelType; + this.Width = width; + this.Height = height; + this.MetaData = metaData; + } + + public PixelTypeInfo PixelType { get; } + + public int Width { get; } + + public int Height { get; } + + public ImageMetaData MetaData { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Image/Image{TPixel}.cs b/src/ImageSharp/Image/Image{TPixel}.cs index 482971e540..2d0448d08f 100644 --- a/src/ImageSharp/Image/Image{TPixel}.cs +++ b/src/ImageSharp/Image/Image{TPixel}.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp /// Encapsulates an image, which consists of the pixel data for a graphics image and its attributes. /// /// The pixel format. - public sealed partial class Image : IDisposable, IConfigurable + public sealed partial class Image : IImage, IDisposable, IConfigurable where TPixel : struct, IPixel { private Configuration configuration; @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp /// The width of the image in pixels. /// The height of the image in pixels. public Image(Configuration configuration, int width, int height) - : this(configuration, width, height, new ImageMetaData()) + : this(configuration, null, width, height, new ImageMetaData()) { } @@ -55,12 +55,14 @@ namespace SixLabors.ImageSharp /// /// The configuration providing initialization code which allows extending the library. /// + /// The information about pixel of the image. /// The width of the image in pixels. /// The height of the image in pixels. /// The images metadata. - internal Image(Configuration configuration, int width, int height, ImageMetaData metadata) + internal Image(Configuration configuration, PixelTypeInfo pixelType, int width, int height, ImageMetaData metadata) { this.configuration = configuration ?? Configuration.Default; + this.PixelType = pixelType; this.MetaData = metadata ?? new ImageMetaData(); this.frames = new ImageFrameCollection(this, width, height); } @@ -70,11 +72,13 @@ namespace SixLabors.ImageSharp /// with the height and the width of the image. /// /// The configuration providing initialization code which allows extending the library. + /// The information about pixel of the image. /// The images metadata. /// The frames that will be owned by this image instance. - internal Image(Configuration configuration, ImageMetaData metadata, IEnumerable> frames) + internal Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetaData metadata, IEnumerable> frames) { this.configuration = configuration ?? Configuration.Default; + this.PixelType = pixelType; this.MetaData = metadata ?? new ImageMetaData(); this.frames = new ImageFrameCollection(this, frames); @@ -84,20 +88,17 @@ namespace SixLabors.ImageSharp /// Gets the pixel buffer. /// Configuration IConfigurable.Configuration => this.configuration; + + /// + public PixelTypeInfo PixelType { get; } - /// - /// Gets the width. - /// + /// public int Width => this.frames.RootFrame.Width; - /// - /// Gets the height. - /// + /// public int Height => this.frames.RootFrame.Height; - /// - /// Gets the meta data of the image. - /// + /// public ImageMetaData MetaData { get; private set; } = new ImageMetaData(); /// @@ -144,7 +145,7 @@ namespace SixLabors.ImageSharp public Image Clone() { IEnumerable> clonedFrames = this.frames.Select(x => x.Clone()); - return new Image(this.configuration, this.MetaData.Clone(), clonedFrames); + return new Image(this.configuration, this.PixelType, this.MetaData.Clone(), clonedFrames); } /// @@ -162,7 +163,7 @@ namespace SixLabors.ImageSharp where TPixel2 : struct, IPixel { IEnumerable> clonedFrames = this.frames.Select(x => x.CloneAs()); - var target = new Image(this.configuration, this.MetaData.Clone(), clonedFrames); + var target = new Image(this.configuration, this.PixelType, this.MetaData.Clone(), clonedFrames); return target; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index 17b42c5040..f1fb0086ff 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors // For resize we know we are going to populate every pixel with fresh data and we want a different target size so // let's manually clone an empty set of images at the correct target and then have the base class process them in turn. IEnumerable> frames = source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); // this will create places holders - var image = new Image(config, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added + var image = new Image(config, source.PixelType, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added return image; } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 17a559a94f..2c0121803b 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); + Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 8838e4f711..9a095548a7 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -130,7 +130,7 @@ namespace SixLabors.ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); + Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index f912a90ae6..cb1987aef4 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -398,7 +398,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); + Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel); } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index b2b4b2f030..248e0a5eea 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - Assert.Equal(expectedPixelSize, Image.DetectPixelType(stream)?.BitsPerPixel); + Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index c12ae5aca4..078b708dfd 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -201,11 +201,6 @@ namespace SixLabors.ImageSharp.Tests return this.testFormat.Sample(); } - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) - { - throw new NotImplementedException(); - } - public bool IsSupportedFileFormat(Span header) => testFormat.IsSupportedFileFormat(header); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index d36fd2a14d..719d1b7583 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -1,19 +1,17 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Drawing; -using System.Drawing.Drawing2D; + using System.IO; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; -using PixelFormat = System.Drawing.Imaging.PixelFormat; - namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { - public class SystemDrawingReferenceDecoder : IImageDecoder + using SixLabors.ImageSharp.MetaData; + + public class SystemDrawingReferenceDecoder : IImageDecoder, IImageInfoDetector { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); @@ -22,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { - if (sourceBitmap.PixelFormat == PixelFormat.Format32bppArgb) + if (sourceBitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb) { return SystemDrawingBridge.FromFromArgb32SystemDrawingBitmap(sourceBitmap); } @@ -32,12 +30,12 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs sourceBitmap.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { - using (var g = Graphics.FromImage(convertedBitmap)) + using (var g = System.Drawing.Graphics.FromImage(convertedBitmap)) { g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; g.DrawImage(sourceBitmap, 0, 0, sourceBitmap.Width, sourceBitmap.Height); } @@ -46,11 +44,12 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) + public IImage Identify(Configuration configuration, Stream stream) { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { - return new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat)); + var pixelType = new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat)); + return new ImageInfo(pixelType, sourceBitmap.Width, sourceBitmap.Height, new ImageMetaData()); } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 0c54f8a3c5..e3249fae9f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -83,11 +83,6 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) - { - throw new NotImplementedException(); - } - // Couldn't make xUnit happy without this hackery: private static ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); @@ -150,11 +145,6 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } - public PixelTypeInfo DetectPixelType(Configuration configuration, Stream stream) - { - throw new NotImplementedException(); - } - private static ConcurrentDictionary invocationCounts = new ConcurrentDictionary(); private string callerName = null; From b435f5eaa5534bf9783f6b939f2a7aefbabd04f6 Mon Sep 17 00:00:00 2001 From: denisivan0v Date: Wed, 17 Jan 2018 17:07:48 +0700 Subject: [PATCH 60/99] codestyle fixes --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 4 ++-- .../Formats/Gif/Sections/GifLogicalScreenDescriptor.cs | 2 +- src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs | 2 +- src/ImageSharp/Image/ImageInfo.cs | 2 +- src/ImageSharp/Image/Image{TPixel}.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index ef9e761643..cb510ac05a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -149,7 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, new ImageMetaData()); } - + /// /// Returns the y- value based on the given height. /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index dbca49f068..257274c92c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -186,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.Gif if (imageDescriptor.LocalColorTableFlag) { int length = imageDescriptor.LocalColorTableSize * 3; - + // Skip local color table block this.Skip(length); } @@ -200,7 +200,7 @@ namespace SixLabors.ImageSharp.Formats.Gif switch (label) { case GifConstants.GraphicControlLabel: - + // Skip graphic control extension block this.Skip(0); break; diff --git a/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs b/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs index 9fea5b1126..05f232a4be 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// rendered in the displaying device. /// public short Height { get; set; } - + /// /// Gets or sets the color depth, in number of bits per pixel. /// diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index b7cad3281e..ef7a4234f8 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// Gets the color depth, in number of bits per pixel. /// public int BitsPerPixel => this.ComponentCount * SupportedPrecision; - + /// /// Gets the image height /// diff --git a/src/ImageSharp/Image/ImageInfo.cs b/src/ImageSharp/Image/ImageInfo.cs index 11dcc3e752..0d8daaf1a8 100644 --- a/src/ImageSharp/Image/ImageInfo.cs +++ b/src/ImageSharp/Image/ImageInfo.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp this.Height = height; this.MetaData = metaData; } - + public PixelTypeInfo PixelType { get; } public int Width { get; } diff --git a/src/ImageSharp/Image/Image{TPixel}.cs b/src/ImageSharp/Image/Image{TPixel}.cs index 2d0448d08f..9b20e3dfab 100644 --- a/src/ImageSharp/Image/Image{TPixel}.cs +++ b/src/ImageSharp/Image/Image{TPixel}.cs @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp /// Gets the pixel buffer. /// Configuration IConfigurable.Configuration => this.configuration; - + /// public PixelTypeInfo PixelType { get; } From 9f719d5b9423083c112a69144e98a15bbb8c9409 Mon Sep 17 00:00:00 2001 From: denisivan0v Date: Wed, 17 Jan 2018 17:11:13 +0700 Subject: [PATCH 61/99] codestyle fix --- src/ImageSharp/Image/IImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Image/IImage.cs b/src/ImageSharp/Image/IImage.cs index f840c78f00..58e5d50e51 100644 --- a/src/ImageSharp/Image/IImage.cs +++ b/src/ImageSharp/Image/IImage.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp /// Gets the width. /// int Width { get; } - + /// /// Gets the height. /// From d86eb2473f1804724c924cf83cceb2779cbb078d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 18 Jan 2018 02:59:46 +0100 Subject: [PATCH 62/99] Use 2 ArrayPool-s with different maxArrayPerBuckets parameters, so we never have more than 8 buckets for large buffers for a single value type. --- src/ImageSharp/Memory/PixelDataPool{T}.cs | 43 ++++++++++++++++--- .../Memory/PixelDataPoolTests.cs | 32 +++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Memory/PixelDataPool{T}.cs b/src/ImageSharp/Memory/PixelDataPool{T}.cs index f25803951c..80c9c410e8 100644 --- a/src/ImageSharp/Memory/PixelDataPool{T}.cs +++ b/src/ImageSharp/Memory/PixelDataPool{T}.cs @@ -17,17 +17,32 @@ namespace SixLabors.ImageSharp.Memory /// The maximum size of pooled arrays in bytes. /// Currently set to 32MB, which is equivalent to 8 megapixels of raw data. /// - private const int MaxPooledBufferSizeInBytes = 32 * 1024 * 1024; + internal const int MaxPooledBufferSizeInBytes = 32 * 1024 * 1024; /// - /// The maximum array length of the . + /// The threshold to pool arrays in which has less buckets for memory safety. /// - private static readonly int MaxArrayLength = MaxPooledBufferSizeInBytes / Unsafe.SizeOf(); + private const int LargeBufferThresholdInBytes = 8 * 1024 * 1024; /// - /// The which is not kept clean. + /// The maximum array length of the . /// - private static readonly ArrayPool ArrayPool = ArrayPool.Create(MaxArrayLength, 50); + private static readonly int MaxLargeArrayLength = MaxPooledBufferSizeInBytes / Unsafe.SizeOf(); + + /// + /// The maximum array length of the . + /// + private static readonly int MaxNormalArrayLength = LargeBufferThresholdInBytes / Unsafe.SizeOf(); + + /// + /// The for huge buffers, which is not kept clean. + /// + private static readonly ArrayPool LargeArrayPool = ArrayPool.Create(MaxLargeArrayLength, 8); + + /// + /// The for small-to-medium buffers which is not kept clean. + /// + private static readonly ArrayPool NormalArrayPool = ArrayPool.Create(MaxNormalArrayLength, 24); /// /// Rents the pixel array from the pool. @@ -36,7 +51,14 @@ namespace SixLabors.ImageSharp.Memory /// The public static T[] Rent(int minimumLength) { - return ArrayPool.Rent(minimumLength); + if (minimumLength <= MaxNormalArrayLength) + { + return NormalArrayPool.Rent(minimumLength); + } + else + { + return LargeArrayPool.Rent(minimumLength); + } } /// @@ -45,7 +67,14 @@ namespace SixLabors.ImageSharp.Memory /// The array to return to the buffer pool. public static void Return(T[] array) { - ArrayPool.Return(array); + if (array.Length <= MaxNormalArrayLength) + { + NormalArrayPool.Return(array); + } + else + { + LargeArrayPool.Return(array); + } } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs b/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs index beaa49dbc2..caba9a4647 100644 --- a/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs +++ b/tests/ImageSharp.Tests/Memory/PixelDataPoolTests.cs @@ -9,19 +9,25 @@ using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Memory { + using System; + /// /// Tests the class. /// public class PixelDataPoolTests { + private const int MaxPooledBufferSizeInBytes = PixelDataPool.MaxPooledBufferSizeInBytes; + readonly object monitor = new object(); - [Fact] - public void PixelDataPoolRentsMinimumSize() + [Theory] + [InlineData(1)] + [InlineData(1024)] + public void PixelDataPoolRentsMinimumSize(int size) { - Rgba32[] pixels = PixelDataPool.Rent(1024); + Rgba32[] pixels = PixelDataPool.Rent(size); - Assert.True(pixels.Length >= 1024); + Assert.True(pixels.Length >= size); } [Fact] @@ -65,17 +71,21 @@ namespace SixLabors.ImageSharp.Tests.Memory } } - [Fact] - public void SmallBuffersArePooled() + [Theory] + [InlineData(32)] + [InlineData(512)] + [InlineData(MaxPooledBufferSizeInBytes-1)] + public void SmallBuffersArePooled(int size) { - Assert.True(this.CheckIsPooled(5, 512)); + Assert.True(this.CheckIsPooled(5, size)); } - [Fact] - public void LargeBuffersAreNotPooled_OfByte() + [Theory] + [InlineData(128 * 1024 * 1024)] + [InlineData(MaxPooledBufferSizeInBytes+1)] + public void LargeBuffersAreNotPooled_OfByte(int size) { - const int mb128 = 128 * 1024 * 1024; - Assert.False(this.CheckIsPooled(2, mb128)); + Assert.False(this.CheckIsPooled(2, size)); } [StructLayout(LayoutKind.Explicit, Size = 512)] From e88e13806fc16d52941521f6a7f25e9b1d024518 Mon Sep 17 00:00:00 2001 From: denisivan0v Date: Thu, 18 Jan 2018 17:13:54 +0700 Subject: [PATCH 63/99] bugfix in GifDecoderCore.Identity --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 11 ---- .../Formats/GeneralFormatTests.cs | 59 +++++++++++++++++++ .../ImageSharp.Tests/ImageSharp.Tests.csproj | 1 + 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 257274c92c..7a08b4194e 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -180,17 +180,6 @@ namespace SixLabors.ImageSharp.Formats.Gif { if (nextFlag == GifConstants.ImageLabel) { - GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); - - // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - if (imageDescriptor.LocalColorTableFlag) - { - int length = imageDescriptor.LocalColorTableSize * 3; - - // Skip local color table block - this.Skip(length); - } - // Skip image block this.Skip(0); } diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 473bc2b523..97128e2c93 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -3,11 +3,19 @@ using System.IO; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using Xunit; namespace SixLabors.ImageSharp.Tests { + using System; + + + public class GeneralFormatTests : FileTestBase { [Theory] @@ -146,5 +154,56 @@ namespace SixLabors.ImageSharp.Tests } } } + + [Theory] + [InlineData(10, 10, "png")] + [InlineData(100, 100, "png")] + [InlineData(100, 10, "png")] + [InlineData(10, 100, "png")] + [InlineData(10, 10, "gif")] + [InlineData(100, 100, "gif")] + [InlineData(100, 10, "gif")] + [InlineData(10, 100, "gif")] + [InlineData(10, 10, "bmp")] + [InlineData(100, 100, "bmp")] + [InlineData(100, 10, "bmp")] + [InlineData(10, 100, "bmp")] + [InlineData(10, 10, "jpg")] + [InlineData(100, 100, "jpg")] + [InlineData(100, 10, "jpg")] + [InlineData(10, 100, "jpg")] + public void CanIdentifyImageLoadedFromBytes(int width, int height, string format) + { + using (Image image = Image.LoadPixelData(new Rgba32[width * height], width, height)) + { + using (var memoryStream = new MemoryStream()) + { + image.Save(memoryStream, GetEncoder(format)); + memoryStream.Position = 0; + + var imageInfo = Image.Identify(memoryStream); + + Assert.Equal(imageInfo.Width, width); + Assert.Equal(imageInfo.Height, height); + } + } + } + + private static IImageEncoder GetEncoder(string format) + { + switch (format) + { + case "png": + return new PngEncoder(); + case "gif": + return new GifEncoder(); + case "bmp": + return new BmpEncoder(); + case "jpg": + return new JpegEncoder(); + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 2f45e4c83a..7e0329ef46 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -17,6 +17,7 @@ + From 61c9caf4727ad8542120aae79142f77da632514e Mon Sep 17 00:00:00 2001 From: denisivan0v Date: Fri, 19 Jan 2018 14:18:01 +0700 Subject: [PATCH 64/99] changes on code review --- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 4 +- src/ImageSharp/Formats/Gif/GifDecoder.cs | 2 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 6 +- src/ImageSharp/Formats/IImageInfoDetector.cs | 6 +- .../Jpeg/GolangPort/OrigJpegDecoder.cs | 2 +- .../Jpeg/GolangPort/OrigJpegDecoderCore.cs | 6 +- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 2 +- .../Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs | 2 +- src/ImageSharp/Formats/PixelTypeInfo.cs | 2 +- src/ImageSharp/Formats/Png/PngDecoder.cs | 2 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 6 +- src/ImageSharp/Image/IImage.cs | 26 +- src/ImageSharp/Image/IImageInfo.cs | 31 +++ src/ImageSharp/Image/Image.Decode.cs | 4 +- src/ImageSharp/Image/Image.FromStream.cs | 12 +- src/ImageSharp/Image/ImageFrameCollection.cs | 4 +- src/ImageSharp/Image/ImageInfo.cs | 16 +- src/ImageSharp/Image/Image{TPixel}.cs | 18 +- src/ImageSharp/ImageSharp.csproj | 236 +++++++++--------- .../Processors/Transforms/ResizeProcessor.cs | 2 +- .../SystemDrawingReferenceDecoder.cs | 5 +- 22 files changed, 208 insertions(+), 188 deletions(-) create mode 100644 src/ImageSharp/Image/IImageInfo.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index e252e63406..78a9de6c45 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index cb510ac05a..e552ac1042 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -141,10 +141,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Reads the image base information from the specified stream. + /// Reads the raw image information from the specified stream. /// /// The containing image data. - public IImage Identify(Stream stream) + public IImageInfo Identify(Stream stream) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, new ImageMetaData()); diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index ccb6cf92c5..c81c51e8b4 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 7a08b4194e..3c22518057 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -165,10 +165,10 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - /// Reads the image base information from the specified stream. + /// Reads the raw image information from the specified stream. /// /// The containing image data. - public IImage Identify(Stream stream) + public IImageInfo Identify(Stream stream) { try { @@ -425,7 +425,7 @@ namespace SixLabors.ImageSharp.Formats.Gif if (previousFrame == null) { // This initializes the image to become fully transparent because the alpha channel is zero. - image = new Image(this.configuration, new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), imageWidth, imageHeight, this.metaData); + image = new Image(this.configuration, imageWidth, imageHeight, this.metaData); this.SetFrameMetaData(image.Frames.RootFrame.MetaData); diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 4e1dfc84f6..37bc0866fa 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -6,16 +6,16 @@ using System.IO; namespace SixLabors.ImageSharp.Formats { /// - /// Used for detecting the image base information without decoding it. + /// Used for detecting the raw image information without decoding it. /// public interface IImageInfoDetector { /// - /// Reads the image base information from the specified stream. + /// Reads the raw image information from the specified stream. /// /// The configuration for the image. /// The containing image data. /// The object - IImage Identify(Configuration configuration, Stream stream); + IImageInfo Identify(Configuration configuration, Stream stream); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs index 7b082a3d6a..ecebe9480d 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoder.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort } /// - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index ef7a4234f8..d788b65c4b 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -198,10 +198,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort } /// - /// Reads the image base information from the specified stream. + /// Reads the raw image information from the specified stream. /// /// The containing image data. - public IImage Identify(Stream stream) + public IImageInfo Identify(Stream stream) { this.ParseStream(stream, true); return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); @@ -789,7 +789,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { using (var postProcessor = new JpegImagePostProcessor(this)) { - var image = new Image(this.configuration, new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); + var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, this.MetaData); postProcessor.PostProcess(image.Frames.RootFrame); return image; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index ee7d7e6996..91835b5d71 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, "stream"); diff --git a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs index c93adac2d4..211c24d208 100644 --- a/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/PdfJsPort/PdfJsJpegDecoderCore.cs @@ -159,7 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.PdfJsPort this.QuantizeAndInverseAllComponents(); - var image = new Image(this.configuration, null, this.ImageWidth, this.ImageHeight, metadata); + var image = new Image(this.configuration, this.ImageWidth, this.ImageHeight, metadata); this.FillPixelData(image.Frames.RootFrame); this.AssignResolution(image); return image; diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index a2a2ce9ce2..cdb6db8d9f 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -1,7 +1,7 @@ namespace SixLabors.ImageSharp.Formats { /// - /// Stores information about pixel. + /// Stores the raw image pixel type information. /// public class PixelTypeInfo { diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 8e110732ee..9bde4f8cc3 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); return decoder.Identify(stream); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index e4c554437e..5c9b753e58 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -279,10 +279,10 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - /// Reads the image base information from the specified stream. + /// Reads the raw image information from the specified stream. /// /// The containing image data. - public IImage Identify(Stream stream) + public IImageInfo Identify(Stream stream) { var metadata = new ImageMetaData(); this.currentStream = stream; @@ -426,7 +426,7 @@ namespace SixLabors.ImageSharp.Formats.Png private void InitializeImage(ImageMetaData metadata, out Image image) where TPixel : struct, IPixel { - image = new Image(this.configuration, new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); + image = new Image(this.configuration, this.header.Width, this.header.Height, metadata); this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; diff --git a/src/ImageSharp/Image/IImage.cs b/src/ImageSharp/Image/IImage.cs index 58e5d50e51..afd00105e6 100644 --- a/src/ImageSharp/Image/IImage.cs +++ b/src/ImageSharp/Image/IImage.cs @@ -1,31 +1,9 @@ -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.MetaData; - -namespace SixLabors.ImageSharp +namespace SixLabors.ImageSharp { /// /// Represents the base image abstraction. /// - public interface IImage + public interface IImage : IImageInfo { - /// - /// Gets information about pixel. - /// - PixelTypeInfo PixelType { get; } - - /// - /// Gets the width. - /// - int Width { get; } - - /// - /// Gets the height. - /// - int Height { get; } - - /// - /// Gets the meta data of the image. - /// - ImageMetaData MetaData { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Image/IImageInfo.cs b/src/ImageSharp/Image/IImageInfo.cs new file mode 100644 index 0000000000..b8dd59cc72 --- /dev/null +++ b/src/ImageSharp/Image/IImageInfo.cs @@ -0,0 +1,31 @@ +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp +{ + /// + /// Represents raw image information. + /// + public interface IImageInfo + { + /// + /// Gets the raw image pixel type information. + /// + PixelTypeInfo PixelType { get; } + + /// + /// Gets the width. + /// + int Width { get; } + + /// + /// Gets the height. + /// + int Height { get; } + + /// + /// Gets the meta data of the image. + /// + ImageMetaData MetaData { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Image/Image.Decode.cs b/src/ImageSharp/Image/Image.Decode.cs index 8f379cb109..6af61f9f4a 100644 --- a/src/ImageSharp/Image/Image.Decode.cs +++ b/src/ImageSharp/Image/Image.Decode.cs @@ -86,9 +86,9 @@ namespace SixLabors.ImageSharp /// The stream. /// the configuration. /// - /// The or null if suitable info detector not found. + /// The or null if suitable info detector not found. /// - private static IImage InternalIdentity(Stream stream, Configuration config) + private static IImageInfo InternalIdentity(Stream stream, Configuration config) { var detector = DiscoverDecoder(stream, config, out IImageFormat _) as IImageInfoDetector; return detector?.Identify(config, stream); diff --git a/src/ImageSharp/Image/Image.FromStream.cs b/src/ImageSharp/Image/Image.FromStream.cs index 0233efb208..680e15aa7d 100644 --- a/src/ImageSharp/Image/Image.FromStream.cs +++ b/src/ImageSharp/Image/Image.FromStream.cs @@ -37,22 +37,22 @@ namespace SixLabors.ImageSharp } /// - /// By reading the header on the provided stream this reads the image base information. + /// By reading the header on the provided stream this reads the raw image information. /// /// The image stream to read the header from. /// /// Thrown if the stream is not readable nor seekable. /// /// - /// The or null if suitable info detector not found. + /// The or null if suitable info detector not found. /// - public static IImage Identify(Stream stream) + public static IImageInfo Identify(Stream stream) { return Identify(null, stream); } /// - /// By reading the header on the provided stream this reads the image base information. + /// By reading the header on the provided stream this reads the raw image information. /// /// The configuration. /// The image stream to read the header from. @@ -60,9 +60,9 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is not readable nor seekable. /// /// - /// The or null if suitable info detector not found. + /// The or null if suitable info detector not found. /// - public static IImage Identify(Configuration config, Stream stream) + public static IImageInfo Identify(Configuration config, Stream stream) { return WithSeekableStream(stream, s => InternalIdentity(s, config ?? Configuration.Default)); } diff --git a/src/ImageSharp/Image/ImageFrameCollection.cs b/src/ImageSharp/Image/ImageFrameCollection.cs index ececcea895..3e9bb03435 100644 --- a/src/ImageSharp/Image/ImageFrameCollection.cs +++ b/src/ImageSharp/Image/ImageFrameCollection.cs @@ -129,7 +129,7 @@ namespace SixLabors.ImageSharp this.frames.Remove(frame); - return new Image(this.parent.GetConfiguration(), this.parent.PixelType, this.parent.MetaData.Clone(), new[] { frame }); + return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame }); } /// @@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp { ImageFrame frame = this[index]; ImageFrame clonedFrame = frame.Clone(); - return new Image(this.parent.GetConfiguration(), this.parent.PixelType, this.parent.MetaData.Clone(), new[] { clonedFrame }); + return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame }); } /// diff --git a/src/ImageSharp/Image/ImageInfo.cs b/src/ImageSharp/Image/ImageInfo.cs index 0d8daaf1a8..a315ee0ed5 100644 --- a/src/ImageSharp/Image/ImageInfo.cs +++ b/src/ImageSharp/Image/ImageInfo.cs @@ -3,8 +3,18 @@ using SixLabors.ImageSharp.MetaData; namespace SixLabors.ImageSharp { - internal sealed class ImageInfo : IImage + /// + /// Stores the raw image information. + /// + internal sealed class ImageInfo : IImageInfo { + /// + /// Initializes a new instance of the class. + /// + /// The raw image pixel type information. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The images metadata. public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetaData metaData) { this.PixelType = pixelType; @@ -13,12 +23,16 @@ namespace SixLabors.ImageSharp this.MetaData = metaData; } + /// public PixelTypeInfo PixelType { get; } + /// public int Width { get; } + /// public int Height { get; } + /// public ImageMetaData MetaData { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Image/Image{TPixel}.cs b/src/ImageSharp/Image/Image{TPixel}.cs index 9b20e3dfab..be38b41f24 100644 --- a/src/ImageSharp/Image/Image{TPixel}.cs +++ b/src/ImageSharp/Image/Image{TPixel}.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp /// The width of the image in pixels. /// The height of the image in pixels. public Image(Configuration configuration, int width, int height) - : this(configuration, null, width, height, new ImageMetaData()) + : this(configuration, width, height, new ImageMetaData()) { } @@ -55,14 +55,13 @@ namespace SixLabors.ImageSharp /// /// The configuration providing initialization code which allows extending the library. /// - /// The information about pixel of the image. /// The width of the image in pixels. /// The height of the image in pixels. /// The images metadata. - internal Image(Configuration configuration, PixelTypeInfo pixelType, int width, int height, ImageMetaData metadata) + internal Image(Configuration configuration, int width, int height, ImageMetaData metadata) { this.configuration = configuration ?? Configuration.Default; - this.PixelType = pixelType; + this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata ?? new ImageMetaData(); this.frames = new ImageFrameCollection(this, width, height); } @@ -72,13 +71,12 @@ namespace SixLabors.ImageSharp /// with the height and the width of the image. /// /// The configuration providing initialization code which allows extending the library. - /// The information about pixel of the image. /// The images metadata. /// The frames that will be owned by this image instance. - internal Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetaData metadata, IEnumerable> frames) + internal Image(Configuration configuration, ImageMetaData metadata, IEnumerable> frames) { this.configuration = configuration ?? Configuration.Default; - this.PixelType = pixelType; + this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata ?? new ImageMetaData(); this.frames = new ImageFrameCollection(this, frames); @@ -145,7 +143,7 @@ namespace SixLabors.ImageSharp public Image Clone() { IEnumerable> clonedFrames = this.frames.Select(x => x.Clone()); - return new Image(this.configuration, this.PixelType, this.MetaData.Clone(), clonedFrames); + return new Image(this.configuration, this.MetaData.Clone(), clonedFrames); } /// @@ -163,7 +161,7 @@ namespace SixLabors.ImageSharp where TPixel2 : struct, IPixel { IEnumerable> clonedFrames = this.frames.Select(x => x.CloneAs()); - var target = new Image(this.configuration, this.PixelType, this.MetaData.Clone(), clonedFrames); + var target = new Image(this.configuration, this.MetaData.Clone(), clonedFrames); return target; } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 8c22237cf7..1d22e59cb2 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -1,120 +1,120 @@  - - A cross-platform library for the processing of image files; written in C# - SixLabors.ImageSharp - $(packageversion) - 0.0.1 - Six Labors and contributors - netstandard1.1;netstandard1.3;netstandard2.0 - true - true - SixLabors.ImageSharp - SixLabors.ImageSharp - Image Resize Crop Gif Jpg Jpeg Bitmap Png Core - https://raw.githubusercontent.com/SixLabors/ImageSharp/master/build/icons/imagesharp-logo-128.png - https://github.com/SixLabors/ImageSharp - http://www.apache.org/licenses/LICENSE-2.0 - git - https://github.com/SixLabors/ImageSharp - false - false - false - false - false - false - false - false - false - full - portable - True - IOperation - - - - - - - - - All - - - - - - - - - - - - - ..\..\ImageSharp.ruleset - SixLabors.ImageSharp - - - true - - - - TextTemplatingFileGenerator - Block8x8F.Generated.cs - - - TextTemplatingFileGenerator - Block8x8F.Generated.cs - - - TextTemplatingFileGenerator - PixelOperations{TPixel}.Generated.cs - - - TextTemplatingFileGenerator - Rgba32.PixelOperations.Generated.cs - - - PorterDuffFunctions.Generated.cs - TextTemplatingFileGenerator - - - DefaultPixelBlenders.Generated.cs - TextTemplatingFileGenerator - - - - - - - - True - True - Block8x8F.Generated.tt - - - True - True - Block8x8F.Generated.tt - - - True - True - PixelOperations{TPixel}.Generated.tt - - - True - True - Rgba32.PixelOperations.Generated.tt - - - True - True - DefaultPixelBlenders.Generated.tt - - - True - True - PorterDuffFunctions.Generated.tt - - + + A cross-platform library for the processing of image files; written in C# + SixLabors.ImageSharp + $(packageversion) + 0.0.1 + Six Labors and contributors + netstandard1.1;netstandard1.3;netstandard2.0 + true + true + SixLabors.ImageSharp + SixLabors.ImageSharp + Image Resize Crop Gif Jpg Jpeg Bitmap Png Core + https://raw.githubusercontent.com/SixLabors/ImageSharp/master/build/icons/imagesharp-logo-128.png + https://github.com/SixLabors/ImageSharp + http://www.apache.org/licenses/LICENSE-2.0 + git + https://github.com/SixLabors/ImageSharp + false + false + false + false + false + false + false + false + false + full + portable + True + IOperation + + + + + + + + + All + + + + + + + + + + + + + ..\..\ImageSharp.ruleset + SixLabors.ImageSharp + + + true + + + + TextTemplatingFileGenerator + Block8x8F.Generated.cs + + + TextTemplatingFileGenerator + Block8x8F.Generated.cs + + + TextTemplatingFileGenerator + PixelOperations{TPixel}.Generated.cs + + + TextTemplatingFileGenerator + Rgba32.PixelOperations.Generated.cs + + + PorterDuffFunctions.Generated.cs + TextTemplatingFileGenerator + + + DefaultPixelBlenders.Generated.cs + TextTemplatingFileGenerator + + + + + + + + True + True + Block8x8F.Generated.tt + + + True + True + Block8x8F.Generated.tt + + + True + True + PixelOperations{TPixel}.Generated.tt + + + True + True + Rgba32.PixelOperations.Generated.tt + + + True + True + DefaultPixelBlenders.Generated.tt + + + True + True + PorterDuffFunctions.Generated.tt + + \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index f1fb0086ff..17b42c5040 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors // For resize we know we are going to populate every pixel with fresh data and we want a different target size so // let's manually clone an empty set of images at the correct target and then have the base class process them in turn. IEnumerable> frames = source.Frames.Select(x => new ImageFrame(this.Width, this.Height, x.MetaData.Clone())); // this will create places holders - var image = new Image(config, source.PixelType, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added + var image = new Image(config, source.MetaData.Clone(), frames); // base the place holder images in to prevent a extra frame being added return image; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 719d1b7583..0e967e9278 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -5,12 +5,11 @@ using System.IO; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { - using SixLabors.ImageSharp.MetaData; - public class SystemDrawingReferenceDecoder : IImageDecoder, IImageInfoDetector { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); @@ -44,7 +43,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public IImage Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream) { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { From a2f0d24893f2ff704b451e2af8f5f4bbcf1ff885 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 19 Jan 2018 21:44:18 +1100 Subject: [PATCH 65/99] Add internal non-affine transform methods --- ImageSharp.sln.DotSettings | 10 + ...ocessor.cs => AffineTransformProcessor.cs} | 138 ++-------- ...cs => CenteredAffineTransformProcessor.cs} | 10 +- .../CenteredNonAffineTransformProcessor.cs | 43 ++++ .../InterpolatedTransformProcessorBase.cs | 139 ++++++++++ .../Transforms/NonAffineTransformProcessor.cs | 238 ++++++++++++++++++ .../Processors/Transforms/RotateProcessor.cs | 2 +- .../Processors/Transforms/SkewProcessor.cs | 2 +- .../Transforms/TransformProcessor.cs | 47 ---- .../Processing/Transforms/Transform.cs | 46 +++- .../Processing/Transforms/TransformHelpers.cs | 29 ++- 11 files changed, 523 insertions(+), 181 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/{AffineProcessor.cs => AffineTransformProcessor.cs} (67%) rename src/ImageSharp/Processing/Processors/Transforms/{CenteredAffineProcessor.cs => CenteredAffineTransformProcessor.cs} (77%) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs diff --git a/ImageSharp.sln.DotSettings b/ImageSharp.sln.DotSettings index 1839bf2f8d..435aad73bf 100644 --- a/ImageSharp.sln.DotSettings +++ b/ImageSharp.sln.DotSettings @@ -38,10 +38,15 @@ NEXT_LINE_SHIFTED_2 1 1 + False + False False + NEVER False False + NEVER False + ALWAYS False True ON_SINGLE_LINE @@ -370,8 +375,13 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True True True + True True True \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs similarity index 67% rename from src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 59b8442639..ce4fbdd712 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -20,35 +18,42 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides the base methods to perform affine transforms on an image. /// /// The pixel format. - internal abstract class AffineProcessor : CloningImageProcessor + internal class AffineTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { private Rectangle targetRectangle; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. + /// + /// The transform matrix + public AffineTransformProcessor(Matrix3x2 matrix) + : this(matrix, KnownResamplers.Bicubic) + { + } + + /// + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - protected AffineProcessor(Matrix3x2 matrix, IResampler sampler) + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) : this(matrix, sampler, Rectangle.Empty) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. /// The rectangle to constrain the transformed image to. - protected AffineProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + : base(sampler) { // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; - - this.Sampler = sampler; - this.targetRectangle = rectangle; } @@ -57,11 +62,6 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public Matrix3x2 TransformMatrix { get; } - /// - /// Gets the sampler to perform interpolation of the transform operation. - /// - public IResampler Sampler { get; } - /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { @@ -85,7 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int width = this.targetRectangle.Width; Rectangle sourceBounds = source.Bounds(); - // Since could potentially be resizing the canvas we need to re-center the matrix + // Since could potentially be resizing the canvas we might need to re-calculate the matrix Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); if (this.Sampler is NearestNeighborResampler) @@ -211,26 +211,6 @@ namespace SixLabors.ImageSharp.Processing.Processors } } - /// - protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) - { - ExifProfile profile = destination.MetaData.ExifProfile; - if (profile == null) - { - return; - } - - if (profile.GetValue(ExifTag.PixelXDimension) != null) - { - profile.SetValue(ExifTag.PixelXDimension, destination.Width); - } - - if (profile.GetValue(ExifTag.PixelYDimension) != null) - { - profile.SetValue(ExifTag.PixelYDimension, destination.Height); - } - } - /// /// Gets a transform matrix adjusted for final processing based upon the target image bounds. /// @@ -254,91 +234,5 @@ namespace SixLabors.ImageSharp.Processing.Processors { return sourceRectangle; } - - /// - /// Calculated the weights for the given point. - /// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. - /// Additionally the weights are nomalized. - /// - /// The minimum sampling offset - /// The maximum sampling offset - /// The minimum source bounds - /// The maximum source bounds - /// The transformed point dimension - /// The sampler - /// The transformed image scale relative to the source - /// The collection of weights - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span weights) - { - float sum = 0; - ref float weightsBaseRef = ref weights[0]; - - // Downsampling weights requires more edge sampling plus normalization of the weights - for (int x = 0, i = min; i <= max; i++, x++) - { - int index = i; - if (index < sourceMin) - { - index = sourceMin; - } - - if (index > sourceMax) - { - index = sourceMax; - } - - float weight = sampler.GetValue((index - point) / scale); - sum += weight; - Unsafe.Add(ref weightsBaseRef, x) = weight; - } - - if (sum > 0) - { - for (int i = 0; i < weights.Length; i++) - { - ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i); - wRef = wRef / sum; - } - } - } - - /// - /// Calculated the weights for the given point. - /// - /// The minimum source bounds - /// The maximum source bounds - /// The transformed point dimension - /// The sampler - /// The collection of weights - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span weights) - { - ref float weightsBaseRef = ref weights[0]; - for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) - { - float weight = sampler.GetValue(i - point); - Unsafe.Add(ref weightsBaseRef, x) = weight; - } - } - - /// - /// Calculates the sampling radius for the current sampler - /// - /// The source dimension size - /// The destination dimension size - /// The radius, and scaling factor - private (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) - { - float ratio = (float)sourceSize / destinationSize; - float scale = ratio; - - if (scale < 1F) - { - scale = 1F; - } - - return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs similarity index 77% rename from src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs index 5631af3aa0..34eabba9b8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs @@ -7,15 +7,19 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors { - internal abstract class CenteredAffineProcessor : AffineProcessor + /// + /// A base class that provides methods to allow the automatic centering of affine transforms + /// + /// The pixel format. + internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessor where TPixel : struct, IPixel { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - protected CenteredAffineProcessor(Matrix3x2 matrix, IResampler sampler) + protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) : base(matrix, sampler) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs new file mode 100644 index 0000000000..bb1505c807 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// A base class that provides methods to allow the automatic centering of non-affine transforms + /// + /// The pixel format. + internal abstract class CenteredNonAffineTransformProcessor : NonAffineTransformProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + protected CenteredNonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler) + : base(matrix, sampler) + { + } + + /// + protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) + { + var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); + var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0); + return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter; + } + + /// + protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) + { + return Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 sizeMatrix) + ? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs new file mode 100644 index 0000000000..5c32f044a2 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// The base class for performing interpolated affine and non-affine transforms. + /// + /// The pixel format. + internal abstract class InterpolatedTransformProcessorBase : CloningImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The sampler to perform the transform operation. + protected InterpolatedTransformProcessorBase(IResampler sampler) + { + this.Sampler = sampler; + } + + /// + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + + /// + /// Calculated the weights for the given point. + /// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Additionally the weights are nomalized. + /// + /// The minimum sampling offset + /// The maximum sampling offset + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The transformed image scale relative to the source + /// The collection of weights + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span weights) + { + float sum = 0; + ref float weightsBaseRef = ref weights[0]; + + // Downsampling weights requires more edge sampling plus normalization of the weights + for (int x = 0, i = min; i <= max; i++, x++) + { + int index = i; + if (index < sourceMin) + { + index = sourceMin; + } + + if (index > sourceMax) + { + index = sourceMax; + } + + float weight = sampler.GetValue((index - point) / scale); + sum += weight; + Unsafe.Add(ref weightsBaseRef, x) = weight; + } + + if (sum > 0) + { + for (int i = 0; i < weights.Length; i++) + { + ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i); + wRef = wRef / sum; + } + } + } + + /// + /// Calculated the weights for the given point. + /// + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The collection of weights + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span weights) + { + ref float weightsBaseRef = ref weights[0]; + for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) + { + float weight = sampler.GetValue(i - point); + Unsafe.Add(ref weightsBaseRef, x) = weight; + } + } + + /// + /// Calculates the sampling radius for the current sampler + /// + /// The source dimension size + /// The destination dimension size + /// The radius, and scaling factor + protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) + { + float ratio = (float)sourceSize / destinationSize; + float scale = ratio; + + if (scale < 1F) + { + scale = 1F; + } + + return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); + } + + /// + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) + { + ExifProfile profile = destination.MetaData.ExifProfile; + if (profile == null) + { + return; + } + + if (profile.GetValue(ExifTag.PixelXDimension) != null) + { + profile.SetValue(ExifTag.PixelXDimension, destination.Width); + } + + if (profile.GetValue(ExifTag.PixelYDimension) != null) + { + profile.SetValue(ExifTag.PixelYDimension, destination.Height); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs new file mode 100644 index 0000000000..caaf812b07 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides the base methods to perform non-affine transforms on an image. + /// + /// The pixel format. + internal class NonAffineTransformProcessor : InterpolatedTransformProcessorBase + where TPixel : struct, IPixel + { + private Rectangle targetRectangle; + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + public NonAffineTransformProcessor(Matrix4x4 matrix) + : this(matrix, KnownResamplers.Bicubic) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + public NonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler) + : this(matrix, sampler, Rectangle.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + /// The rectangle to constrain the transformed image to. + public NonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) + : base(sampler) + { + // Tansforms are inverted else the output is the opposite of the expected. + Matrix4x4.Invert(matrix, out matrix); + this.TransformMatrix = matrix; + this.targetRectangle = rectangle; + } + + /// + /// Gets the matrix used to supply the non-affine transform + /// + public Matrix4x4 TransformMatrix { get; } + + /// + protected override Image CreateDestination(Image source, Rectangle sourceRectangle) + { + if (this.targetRectangle == Rectangle.Empty) + { + this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix); + } + + // We will always be creating the clone even for mutate because we may need to resize the canvas + IEnumerable> frames = + source.Frames.Select(x => new ImageFrame(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone())); + + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + } + + /// + protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + { + int height = this.targetRectangle.Height; + int width = this.targetRectangle.Width; + Rectangle sourceBounds = source.Bounds(); + + // Since could potentially be resizing the canvas we might need to re-calculate the matrix + Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); + + if (this.Sampler is NearestNeighborResampler) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var point = Point.Round(Vector2.Transform(new Vector2(x, y), matrix)); + if (sourceBounds.Contains(point.X, point.Y)) + { + destRow[x] = source[point.X, point.Y]; + } + } + }); + + return; + } + + int maxSourceX = source.Width - 1; + int maxSourceY = source.Height - 1; + (float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + (float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + float xScale = xRadiusScale.scale; + float yScale = yRadiusScale.scale; + var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); + IResampler sampler = this.Sampler; + var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); + int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); + int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); + + using (var yBuffer = new Buffer2D(yLength, height)) + using (var xBuffer = new Buffer2D(xLength, height)) + { + Parallel.For( + 0, + height, + configuration.ParallelOptions, + y => + { + Span destRow = destination.GetPixelRowSpan(y); + Span ySpan = yBuffer.GetRowSpan(y); + Span xSpan = xBuffer.GetRowSpan(y); + + for (int x = 0; x < width; x++) + { + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var point = Vector2.Transform(new Vector2(x, y), matrix); + + // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; + + // max, maxY, minX, minY + var extents = new Vector4( + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F), + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F)); + + int right = (int)extents.X; + int bottom = (int)extents.Y; + int left = (int)extents.Z; + int top = (int)extents.W; + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; + + if (minX == maxX || minY == maxY) + { + continue; + } + + // It appears these have to be calculated on-the-fly. + // Precalulating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions on both axis. + // I've optimized where I can but am always open to suggestions. + if (yScale > 1 && xScale > 1) + { + CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); + CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); + } + + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) + { + float yWeight = ySpan[yy]; + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = xSpan[xx]; + var vector = source[i, j].ToVector4(); + + // Values are first premultiplied to prevent darkening of edge pixels + var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); + sum += mupltiplied * xWeight * yWeight; + } + } + + ref TPixel dest = ref destRow[x]; + + // Reverse the premultiplication + dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); + } + }); + } + } + + /// + /// Gets a transform matrix adjusted for final processing based upon the target image bounds. + /// + /// The source image bounds. + /// The destination image bounds. + /// + /// The . + /// + protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) + { + return this.TransformMatrix; + } + + /// + /// Gets the bounding relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// The + protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) + { + return sourceRectangle; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index fa47dadaaa..b93c869152 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : CenteredAffineProcessor + internal class RotateProcessor : CenteredAffineTransformProcessor where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index b123a309be..8c47b35274 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : CenteredAffineProcessor + internal class SkewProcessor : CenteredAffineTransformProcessor where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs deleted file mode 100644 index 140fd7ec6a..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Provides methods that allow the tranformation of images using various algorithms. - /// - /// The pixel format. - internal class TransformProcessor : AffineProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transformation matrix - public TransformProcessor(Matrix3x2 matrix) - : this(matrix, KnownResamplers.Bicubic) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The transformation matrix - /// The sampler to perform the transform operation. - public TransformProcessor(Matrix3x2 matrix, IResampler sampler) - : base(matrix, sampler) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The rectangle to constrain the transformed image to. - public TransformProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) - : base(matrix, sampler, rectangle) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index 74f91fa701..cbfe6da187 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -18,18 +18,18 @@ namespace SixLabors.ImageSharp /// Transforms an image by the given matrix. /// /// The pixel format. - /// The image to skew. + /// The image to transform. /// The transformation matrix. /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix) where TPixel : struct, IPixel - => Transform(source, matrix, KnownResamplers.NearestNeighbor); + => Transform(source, matrix, KnownResamplers.NearestNeighbor); /// /// Transforms an image by the given matrix using the specified sampling algorithm. /// /// The pixel format. - /// The image to skew. + /// The image to transform. /// The transformation matrix. /// The to perform the resampling. /// The @@ -41,13 +41,49 @@ namespace SixLabors.ImageSharp /// Transforms an image by the given matrix using the specified sampling algorithm. /// /// The pixel format. - /// The image to skew. + /// The image to transform. /// The transformation matrix. /// The to perform the resampling. /// The rectangle to constrain the transformed image to. /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new TransformProcessor(matrix, sampler, rectangle)); + => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, rectangle)); + + /// + /// Transforms an image by the given matrix. + /// + /// The pixel format. + /// The image to transform. + /// The transformation matrix. + /// The + internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix) + where TPixel : struct, IPixel + => Transform(source, matrix, KnownResamplers.NearestNeighbor); + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to transform. + /// The transformation matrix. + /// The to perform the resampling. + /// The + internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) + where TPixel : struct, IPixel + => Transform(source, matrix, sampler, Rectangle.Empty); + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to transform. + /// The transformation matrix. + /// The to perform the resampling. + /// The rectangle to constrain the transformed image to. + /// The + internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) + where TPixel : struct, IPixel + => source.ApplyProcessor(new NonAffineTransformProcessor(matrix, sampler, rectangle)); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs index 419c1c13d6..119fc9eeda 100644 --- a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs @@ -8,7 +8,7 @@ using SixLabors.Primitives; namespace SixLabors.ImageSharp { /// - /// Contains helper methods for working with affine transforms + /// Contains helper methods for working with affine and non-affine transforms /// internal class TransformHelpers { @@ -29,7 +29,32 @@ namespace SixLabors.ImageSharp var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); - // Find the minimum and maximum "corners" based on the ones above + return GetBoundingRectangle(tl, tr, bl, br); + } + + /// + /// Returns the bounding relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix4x4 matrix) + { + // Calculate the position of the four corners in world space by applying + // The world matrix to the four corners in object space (0, 0, width, height) + var tl = Vector2.Transform(Vector2.Zero, matrix); + var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix); + var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix); + var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix); + + return GetBoundingRectangle(tl, tr, bl, br); + } + + private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) + { + // Find the minimum and maximum "corners" based on the given vectors float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); From f1c8bc03487b8d15f4d5ef31fac9c8b2751ccbee Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 19 Jan 2018 23:12:14 +1100 Subject: [PATCH 66/99] Update intellisens docs + copyright --- ImageSharp.sln.DotSettings | 10 ++++++++++ src/ImageSharp/Formats/IImageInfoDetector.cs | 2 +- .../Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs | 1 + src/ImageSharp/Formats/PixelTypeInfo.cs | 7 +++++-- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 4 ++-- src/ImageSharp/Image/IImage.cs | 7 +++++-- src/ImageSharp/Image/IImageInfo.cs | 12 ++++++++---- src/ImageSharp/Image/Image.Decode.cs | 2 +- src/ImageSharp/Image/Image.FromStream.cs | 6 +++--- src/ImageSharp/Image/ImageInfo.cs | 9 ++++++--- 10 files changed, 42 insertions(+), 18 deletions(-) diff --git a/ImageSharp.sln.DotSettings b/ImageSharp.sln.DotSettings index 1839bf2f8d..435aad73bf 100644 --- a/ImageSharp.sln.DotSettings +++ b/ImageSharp.sln.DotSettings @@ -38,10 +38,15 @@ NEXT_LINE_SHIFTED_2 1 1 + False + False False + NEVER False False + NEVER False + ALWAYS False True ON_SINGLE_LINE @@ -370,8 +375,13 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True True True + True True True \ No newline at end of file diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 37bc0866fa..b7769e8955 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -6,7 +6,7 @@ using System.IO; namespace SixLabors.ImageSharp.Formats { /// - /// Used for detecting the raw image information without decoding it. + /// Encapsulates methods used for detecting the raw image information without fully decoding it. /// public interface IImageInfoDetector { diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index d788b65c4b..cdc2a91972 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -204,6 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort public IImageInfo Identify(Stream stream) { this.ParseStream(stream, true); + return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); } diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index cdb6db8d9f..ed21b91bfc 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -1,7 +1,10 @@ -namespace SixLabors.ImageSharp.Formats +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats { /// - /// Stores the raw image pixel type information. + /// Contains information about the pixels that make up an images visual data. /// public class PixelTypeInfo { diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5c9b753e58..5cdf80289c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -332,10 +332,10 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.header == null) { - throw new ImageFormatException("PNG Image hasn't header chunk"); + throw new ImageFormatException("PNG Image does not contain a header chunk"); } - return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); } /// diff --git a/src/ImageSharp/Image/IImage.cs b/src/ImageSharp/Image/IImage.cs index afd00105e6..7355dc1fec 100644 --- a/src/ImageSharp/Image/IImage.cs +++ b/src/ImageSharp/Image/IImage.cs @@ -1,7 +1,10 @@ -namespace SixLabors.ImageSharp +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp { /// - /// Represents the base image abstraction. + /// Encapsulates the properties and methods that describe an image. /// public interface IImage : IImageInfo { diff --git a/src/ImageSharp/Image/IImageInfo.cs b/src/ImageSharp/Image/IImageInfo.cs index b8dd59cc72..25d5ec7cab 100644 --- a/src/ImageSharp/Image/IImageInfo.cs +++ b/src/ImageSharp/Image/IImageInfo.cs @@ -1,15 +1,19 @@ -using SixLabors.ImageSharp.Formats; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData; namespace SixLabors.ImageSharp { /// - /// Represents raw image information. + /// Encapsulates properties that descibe basic image information including dimensions, pixel type information + /// and additional metadata /// public interface IImageInfo { /// - /// Gets the raw image pixel type information. + /// Gets information about the image pixels. /// PixelTypeInfo PixelType { get; } @@ -24,7 +28,7 @@ namespace SixLabors.ImageSharp int Height { get; } /// - /// Gets the meta data of the image. + /// Gets the metadata of the image. /// ImageMetaData MetaData { get; } } diff --git a/src/ImageSharp/Image/Image.Decode.cs b/src/ImageSharp/Image/Image.Decode.cs index 6af61f9f4a..ef00ff15fd 100644 --- a/src/ImageSharp/Image/Image.Decode.cs +++ b/src/ImageSharp/Image/Image.Decode.cs @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp } /// - /// Reads the image base information. + /// Reads the raw image information from the specified stream. /// /// The stream. /// the configuration. diff --git a/src/ImageSharp/Image/Image.FromStream.cs b/src/ImageSharp/Image/Image.FromStream.cs index 680e15aa7d..62668dd023 100644 --- a/src/ImageSharp/Image/Image.FromStream.cs +++ b/src/ImageSharp/Image/Image.FromStream.cs @@ -52,15 +52,15 @@ namespace SixLabors.ImageSharp } /// - /// By reading the header on the provided stream this reads the raw image information. + /// Reads the raw image information from the specified stream without fully decoding it. /// /// The configuration. - /// The image stream to read the header from. + /// The image stream to read the information from. /// /// Thrown if the stream is not readable nor seekable. /// /// - /// The or null if suitable info detector not found. + /// The or null if suitable info detector is not found. /// public static IImageInfo Identify(Configuration config, Stream stream) { diff --git a/src/ImageSharp/Image/ImageInfo.cs b/src/ImageSharp/Image/ImageInfo.cs index a315ee0ed5..6f894cb599 100644 --- a/src/ImageSharp/Image/ImageInfo.cs +++ b/src/ImageSharp/Image/ImageInfo.cs @@ -1,17 +1,20 @@ -using SixLabors.ImageSharp.Formats; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData; namespace SixLabors.ImageSharp { /// - /// Stores the raw image information. + /// Contains information about the image including dimensions, pixel type information and additional metadata /// internal sealed class ImageInfo : IImageInfo { /// /// Initializes a new instance of the class. /// - /// The raw image pixel type information. + /// The image pixel type information. /// The width of the image in pixels. /// The height of the image in pixels. /// The images metadata. From 12f135825545c004d1a4ccd8474432b293b8ff5c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sat, 20 Jan 2018 21:54:18 +0100 Subject: [PATCH 67/99] fix merge tool madness --- src/ImageSharp/ImageSharp.csproj | 120 +------------------------------ 1 file changed, 1 insertion(+), 119 deletions(-) diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 82282fb81c..9d30030da6 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -1,122 +1,4 @@  - - A cross-platform library for the processing of image files; written in C# - SixLabors.ImageSharp - $(packageversion) - 0.0.1 - Six Labors and contributors - netstandard1.1;netstandard1.3;netstandard2.0 - true - true - SixLabors.ImageSharp - SixLabors.ImageSharp - Image Resize Crop Gif Jpg Jpeg Bitmap Png Core - https://raw.githubusercontent.com/SixLabors/ImageSharp/master/build/icons/imagesharp-logo-128.png - https://github.com/SixLabors/ImageSharp - http://www.apache.org/licenses/LICENSE-2.0 - git - https://github.com/SixLabors/ImageSharp - false - false - false - false - false - false - false - false - false - full - portable - True - IOperation - - - - - - - - - All - - - - - - - - - - - - - ..\..\ImageSharp.ruleset - SixLabors.ImageSharp - - - true - - - - TextTemplatingFileGenerator - Block8x8F.Generated.cs - - - TextTemplatingFileGenerator - Block8x8F.Generated.cs - - - TextTemplatingFileGenerator - PixelOperations{TPixel}.Generated.cs - - - TextTemplatingFileGenerator - Rgba32.PixelOperations.Generated.cs - - - PorterDuffFunctions.Generated.cs - TextTemplatingFileGenerator - - - DefaultPixelBlenders.Generated.cs - TextTemplatingFileGenerator - - - - - - - - True - True - Block8x8F.Generated.tt - - - True - True - Block8x8F.Generated.tt - - - True - True - PixelOperations{TPixel}.Generated.tt - - - True - True - Rgba32.PixelOperations.Generated.tt - - - True - True - DefaultPixelBlenders.Generated.tt - - - True - True - PorterDuffFunctions.Generated.tt - - A cross-platform library for the processing of image files; written in C# SixLabors.ImageSharp @@ -152,7 +34,7 @@ - + All From 3705fe848634be6731aa6d421c72917495acd172 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 01:13:29 +0100 Subject: [PATCH 68/99] better TransformTests --- .../Processing/Transforms/TransformTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index d5e1f144a1..43ae664184 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { 0, 1, 1, -20, -10 }, { 50, 1, 1, -20, -10 }, { 50, 1.5f, 1.5f, 0, 0 }, - { 50, 1.1F, 1.2F, 20, 10 }, + { 50, 1.1F, 1.3F, 30, -20 }, { 0, 2f, 1f, 0, 0 }, { 0, 1f, 2f, 0, 0 }, }; @@ -113,7 +113,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms this.PrintMatrix(m); image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); - image.DebugSave(provider, $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"); + + string testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"; + image.DebugSave(provider, testOutputDetails); + image.CompareToReferenceOutput(provider, testOutputDetails); } } From ef8c36249757b6810242849d04817c89ca0d443e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 01:18:14 +0100 Subject: [PATCH 69/99] TransformTests -> AffineTransformTests + update submodule to use new reference images --- .../Processing/Transforms/TransformTests.cs | 7 +++---- tests/Images/External | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 43ae664184..574a5c3bab 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -6,13 +6,12 @@ using SixLabors.ImageSharp.Processing; using SixLabors.Primitives; using Xunit; using Xunit.Abstractions; +using SixLabors.ImageSharp.Helpers; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - using SixLabors.ImageSharp.Helpers; - - public class TransformTests + public class AffineTransformTests { private readonly ITestOutputHelper Output; @@ -63,7 +62,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms nameof(KnownResamplers.Lanczos8), }; - public TransformTests(ITestOutputHelper output) + public AffineTransformTests(ITestOutputHelper output) { this.Output = output; } diff --git a/tests/Images/External b/tests/Images/External index dc5479d00b..0141b2af86 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit dc5479d00b2312f691e6249b9f7765e2316d4a30 +Subproject commit 0141b2af864fa3eaa63b09127dc99fb09374f235 From 23c5d1fdf5939d93698f9995f19ba0e6d73b840e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 01:37:35 +0100 Subject: [PATCH 70/99] AffineTransformProcessor.targetRectangle replaced by targetDimensions --- .../Transforms/AffineTransformProcessor.cs | 34 ++++++++++++------- .../CenteredAffineTransformProcessor.cs | 14 +++++--- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index ce4fbdd712..fb07eb0238 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class AffineTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { - private Rectangle targetRectangle; + private Size? targetDimensions; /// /// Initializes a new instance of the class. @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Processing.Processors // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; - this.targetRectangle = rectangle; + this.targetDimensions = rectangle == Rectangle.Empty ?(Size?)null : rectangle.Size; } /// @@ -65,28 +65,38 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - if (this.targetRectangle == Rectangle.Empty) + if (!this.targetDimensions.HasValue) { - this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix); + // TODO: CreateDestination() should not modify the processors state! (kinda CQRS) + this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix); } + Size targetDims = this.targetDimensions.Value; + // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(targetDims.Width, targetDims.Height, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); } /// - protected override void OnApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) + protected override void OnApply( + ImageFrame source, + ImageFrame destination, + Rectangle sourceRectangle, + Configuration configuration) { - int height = this.targetRectangle.Height; - int width = this.targetRectangle.Width; + int height = this.targetDimensions.Value.Height; + int width = this.targetDimensions.Value.Width; + Rectangle sourceBounds = source.Bounds(); + var targetBounds = new Rectangle(0, 0, width, height); // Since could potentially be resizing the canvas we might need to re-calculate the matrix - Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); + + Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); if (this.Sampler is NearestNeighborResampler) { @@ -227,12 +237,12 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// Gets the bounding relative to the source for the given transformation matrix. /// - /// The source rectangle. + /// The source rectangle. /// The transformation matrix. /// The - protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix3x2 matrix) + protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) { - return sourceRectangle; + return sourceDimensions; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs index 34eabba9b8..34a0866615 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs @@ -33,11 +33,17 @@ namespace SixLabors.ImageSharp.Processing.Processors } /// - protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix3x2 matrix) + protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) { - return Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix) - ? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; + var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); + + if (!Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix)) + { + // TODO: Shouldn't we throw an exception instead? + return sourceDimensions; + } + + return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix).Size; } } } \ No newline at end of file From a8c5355651990e2723c0814ed3cfb6dc25f3142c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 01:49:54 +0100 Subject: [PATCH 71/99] Transform() extension method overload taking Size --- src/ImageSharp/Image/ImageFrame{TPixel}.cs | 12 ++++++++ .../Transforms/AffineTransformProcessor.cs | 21 ++++++------- .../Processing/Transforms/Transform.cs | 30 +++++++++++++++++-- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Image/ImageFrame{TPixel}.cs b/src/ImageSharp/Image/ImageFrame{TPixel}.cs index 45ed5f053a..96f78ef98a 100644 --- a/src/ImageSharp/Image/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/Image/ImageFrame{TPixel}.cs @@ -13,6 +13,8 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp { + using SixLabors.Primitives; + /// /// Represents a single frame in a animation. /// @@ -53,6 +55,16 @@ namespace SixLabors.ImageSharp this.MetaData = metaData; } + /// + /// Initializes a new instance of the class. + /// + /// The of the frame. + /// The meta data. + internal ImageFrame(Size size, ImageFrameMetaData metaData) + : this(size.Width, size.Height, metaData) + { + } + /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index fb07eb0238..07c247d734 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class AffineTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { - private Size? targetDimensions; + private Size targetDimensions; /// /// Initializes a new instance of the class. @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The transform matrix /// The sampler to perform the transform operation. public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) - : this(matrix, sampler, Rectangle.Empty) + : this(matrix, sampler, Size.Empty) { } @@ -47,14 +47,14 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The transform matrix /// The sampler to perform the transform operation. - /// The rectangle to constrain the transformed image to. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + /// The target dimensions to constrain the transformed image to. + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) : base(sampler) { // Tansforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; - this.targetDimensions = rectangle == Rectangle.Empty ?(Size?)null : rectangle.Size; + this.targetDimensions = targetDimensions; } /// @@ -65,17 +65,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { - if (!this.targetDimensions.HasValue) + if (this.targetDimensions == Size.Empty) { // TODO: CreateDestination() should not modify the processors state! (kinda CQRS) this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix); } - Size targetDims = this.targetDimensions.Value; - // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(targetDims.Width, targetDims.Height, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(this.targetDimensions, x.MetaData.Clone())); // Use the overload to prevent an extra frame being added return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); @@ -88,14 +86,13 @@ namespace SixLabors.ImageSharp.Processing.Processors Rectangle sourceRectangle, Configuration configuration) { - int height = this.targetDimensions.Value.Height; - int width = this.targetDimensions.Value.Width; + int height = this.targetDimensions.Height; + int width = this.targetDimensions.Width; Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(0, 0, width, height); // Since could potentially be resizing the canvas we might need to re-calculate the matrix - Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); if (this.Sampler is NearestNeighborResampler) diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index cbfe6da187..87c7306b81 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -46,9 +46,35 @@ namespace SixLabors.ImageSharp /// The to perform the resampling. /// The rectangle to constrain the transformed image to. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler, Rectangle rectangle) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Matrix3x2 matrix, + IResampler sampler, + Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, rectangle)); + { + // TODO: Fixme! + return source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, rectangle.Size)); + } + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to transform. + /// The transformation matrix. + /// The to perform the resampling. + /// The dimensions to constrain the transformed image to. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + Matrix3x2 matrix, + IResampler sampler, + Size destinationSize) + where TPixel : struct, IPixel + { + return source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, destinationSize)); + } /// /// Transforms an image by the given matrix. From e6fee1bbda5c2e6028919487d75451ab64e0f380 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 01:59:00 +0100 Subject: [PATCH 72/99] Transform_RotateScale_ManuallyCentered uses reference image now --- .../{TransformTests.cs => AffineTransformTests.cs} | 5 ++++- tests/Images/External | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename tests/ImageSharp.Tests/Processing/Transforms/{TransformTests.cs => AffineTransformTests.cs} (97%) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs similarity index 97% rename from tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs rename to tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 574a5c3bab..f6ef365e54 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -129,7 +129,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image); image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); - image.DebugSave(provider, $"R({angleDeg})_S({s})"); + + string testOutputDetails = $"R({angleDeg})_S({s})"; + image.DebugSave(provider, testOutputDetails); + image.CompareToReferenceOutput(provider, testOutputDetails); } } diff --git a/tests/Images/External b/tests/Images/External index 0141b2af86..9d4070dab9 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 0141b2af864fa3eaa63b09127dc99fb09374f235 +Subproject commit 9d4070dab9e4aefa70dcd41ef98176a5d54e644f From 25763f0f999fc1e038beb874597a0a4624f6e7fb Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 03:34:21 +0100 Subject: [PATCH 73/99] finalized transform with source rectangle + tests --- .../Processing/Transforms/Transform.cs | 11 ++-- .../Transforms/AffineTransformTests.cs | 52 ++++++++++++++----- tests/Images/External | 2 +- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index 87c7306b81..496e57defc 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) where TPixel : struct, IPixel - => Transform(source, matrix, sampler, Rectangle.Empty); + => Transform(source, matrix, sampler, Size.Empty); /// /// Transforms an image by the given matrix using the specified sampling algorithm. @@ -44,17 +44,18 @@ namespace SixLabors.ImageSharp /// The image to transform. /// The transformation matrix. /// The to perform the resampling. - /// The rectangle to constrain the transformed image to. + /// The rectangle defining the source pixel area to transform. 'sourceRectangle.Location' becomes the origo of the transformed image. /// The public static IImageProcessingContext Transform( this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler, - Rectangle rectangle) + Rectangle sourceRectangle) where TPixel : struct, IPixel { - // TODO: Fixme! - return source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, rectangle.Size)); + var t = Matrix3x2.CreateTranslation(-sourceRectangle.Location); + Matrix3x2 combinedMatrix = t * matrix; + return source.ApplyProcessor(new AffineTransformProcessor(combinedMatrix, sampler, sourceRectangle.Size)); } /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index f6ef365e54..ace6cb70d6 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -143,40 +143,66 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { 0, 0, 5, 10 }, { 0, 0, 10, 5 }, { 5, 0, 5, 10 }, - {-5,-5, 15, 15 } + {-5,-5, 20, 20 } }; + /// + /// Testing transforms using custom source rectangles: + /// https://github.com/SixLabors/ImageSharp/pull/386#issuecomment-357104963 + /// + [Theory] + [WithTestPatternImages(96, 48, PixelTypes.Rgba32)] + public void Transform_FromSourceRectangle1(TestImageProvider provider) + where TPixel : struct, IPixel + { + var rectangle = new Rectangle(48, 0, 96, 36); + + using (Image image = provider.GetImage()) + { + var m = Matrix3x2.CreateScale(2.0F, 1.5F); + + image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + } + [Theory] - [WithSolidFilledImages(nameof(Transform_IntoRectangle_Data), 10, 10, nameof(Rgba32.Red), PixelTypes.Rgba32)] - public void Transform_IntoRectangle(TestImageProvider provider, int x0, int y0, int w, int h) + [WithTestPatternImages(96, 48, PixelTypes.Rgba32)] + public void Transform_FromSourceRectangle2(TestImageProvider provider) where TPixel : struct, IPixel { - var rectangle = new Rectangle(x0, y0, w, h); + var rectangle = new Rectangle(0, 24, 48, 48); using (Image image = provider.GetImage()) { - Matrix3x2 m = this.MakeManuallyCenteredMatrix(45, 0.8f, image); + var m = Matrix3x2.CreateScale(1.0F, 2.0F); image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); - string testDetails = $"({x0},{y0}-W{w},H{h})"; - image.DebugSave(provider, testDetails); + + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); } } [Theory] - [WithTestPatternImages(nameof(ResamplerNames), 100, 200, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] public void Transform_WithSampler(TestImageProvider provider, string resamplerName) where TPixel : struct, IPixel { IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(50); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.5F, .5F)); - var translate = Matrix3x2.CreateTranslation(75, 0); - - image.Mutate(i => i.Transform(rotate * scale * translate, sampler)); + Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image); + + image.Mutate(i => + { + i.Transform(m, sampler); + }); + image.DebugSave(provider, resamplerName); + image.CompareToReferenceOutput(provider, resamplerName); } } diff --git a/tests/Images/External b/tests/Images/External index 9d4070dab9..02687e772f 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 9d4070dab9e4aefa70dcd41ef98176a5d54e644f +Subproject commit 02687e772f84b1d518ab83eacdfafba476f824e6 From 56a16f6a22521f60fb87a7fcf278aec3021ec88a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 03:47:24 +0100 Subject: [PATCH 74/99] IDE-s & analyzers why are you doing this to me? It's 2018! --- src/ImageSharp/Image/ImageFrame{TPixel}.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Image/ImageFrame{TPixel}.cs b/src/ImageSharp/Image/ImageFrame{TPixel}.cs index 96f78ef98a..e39cc1ab2f 100644 --- a/src/ImageSharp/Image/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/Image/ImageFrame{TPixel}.cs @@ -10,11 +10,10 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; namespace SixLabors.ImageSharp { - using SixLabors.Primitives; - /// /// Represents a single frame in a animation. /// From ce5b62471acf0fa7ef4b8a96767a57d6907a519f Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 03:55:11 +0100 Subject: [PATCH 75/99] renamed NonAffineTransformProcessor to ProjectiveTransformProcessor --- ...cs => CenteredProjectiveTransformProcessor.cs} | 6 +++--- ...ocessor.cs => ProjectiveTransformProcessor.cs} | 15 ++++++++------- src/ImageSharp/Processing/Transforms/Transform.cs | 8 +++++--- 3 files changed, 16 insertions(+), 13 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/{CenteredNonAffineTransformProcessor.cs => CenteredProjectiveTransformProcessor.cs} (87%) rename src/ImageSharp/Processing/Processors/Transforms/{NonAffineTransformProcessor.cs => ProjectiveTransformProcessor.cs} (93%) diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs similarity index 87% rename from src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs index bb1505c807..dc2dd28ab1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredNonAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs @@ -11,15 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors /// A base class that provides methods to allow the automatic centering of non-affine transforms /// /// The pixel format. - internal abstract class CenteredNonAffineTransformProcessor : NonAffineTransformProcessor + internal abstract class CenteredProjectiveTransformProcessor : ProjectiveTransformProcessor where TPixel : struct, IPixel { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - protected CenteredNonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler) + protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) : base(matrix, sampler) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs similarity index 93% rename from src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index caaf812b07..579a0ac9a2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/NonAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -16,39 +16,40 @@ namespace SixLabors.ImageSharp.Processing.Processors { /// /// Provides the base methods to perform non-affine transforms on an image. + /// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior /// /// The pixel format. - internal class NonAffineTransformProcessor : InterpolatedTransformProcessorBase + internal class ProjectiveTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { private Rectangle targetRectangle; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix - public NonAffineTransformProcessor(Matrix4x4 matrix) + public ProjectiveTransformProcessor(Matrix4x4 matrix) : this(matrix, KnownResamplers.Bicubic) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - public NonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler) + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) : this(matrix, sampler, Rectangle.Empty) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. /// The rectangle to constrain the transformed image to. - public NonAffineTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) : base(sampler) { // Tansforms are inverted else the output is the opposite of the expected. diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index 496e57defc..21cd7863a5 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -89,7 +89,8 @@ namespace SixLabors.ImageSharp => Transform(source, matrix, KnownResamplers.NearestNeighbor); /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. + /// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior /// /// The pixel format. /// The image to transform. @@ -101,7 +102,8 @@ namespace SixLabors.ImageSharp => Transform(source, matrix, sampler, Rectangle.Empty); /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. + /// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior /// /// The pixel format. /// The image to transform. @@ -111,6 +113,6 @@ namespace SixLabors.ImageSharp /// The internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) where TPixel : struct, IPixel - => source.ApplyProcessor(new NonAffineTransformProcessor(matrix, sampler, rectangle)); + => source.ApplyProcessor(new ProjectiveTransformProcessor(matrix, sampler, rectangle)); } } \ No newline at end of file From c1ce22bb3318d4d51ff2db0bf8f0b34aed0a7ed0 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 14:01:45 +0100 Subject: [PATCH 76/99] better variable naming and docs --- .../Transforms/ProjectiveTransformProcessor.cs | 1 + .../Processing/Transforms/Transform.cs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 579a0ac9a2..f53585093b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -22,6 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors internal class ProjectiveTransformProcessor : InterpolatedTransformProcessorBase where TPixel : struct, IPixel { + // TODO: We should use a Size instead! (See AffineTransformProcessor) private Rectangle targetRectangle; /// diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs index 21cd7863a5..e39da8dc0f 100644 --- a/src/ImageSharp/Processing/Transforms/Transform.cs +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -38,34 +38,38 @@ namespace SixLabors.ImageSharp => Transform(source, matrix, sampler, Size.Empty); /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Transforms an image by the given matrix using the specified sampling algorithm + /// and a rectangle defining the transform origin in the source image and the size of the result image. /// /// The pixel format. /// The image to transform. /// The transformation matrix. /// The to perform the resampling. - /// The rectangle defining the source pixel area to transform. 'sourceRectangle.Location' becomes the origo of the transformed image. + /// + /// The rectangle defining the transform origin in the source image, and the size of the result image. + /// /// The public static IImageProcessingContext Transform( this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler, - Rectangle sourceRectangle) + Rectangle rectangle) where TPixel : struct, IPixel { - var t = Matrix3x2.CreateTranslation(-sourceRectangle.Location); + var t = Matrix3x2.CreateTranslation(-rectangle.Location); Matrix3x2 combinedMatrix = t * matrix; - return source.ApplyProcessor(new AffineTransformProcessor(combinedMatrix, sampler, sourceRectangle.Size)); + return source.ApplyProcessor(new AffineTransformProcessor(combinedMatrix, sampler, rectangle.Size)); } /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Transforms an image by the given matrix using the specified sampling algorithm, + /// cropping or extending the image according to . /// /// The pixel format. /// The image to transform. /// The transformation matrix. /// The to perform the resampling. - /// The dimensions to constrain the transformed image to. + /// The size of the destination image. /// The public static IImageProcessingContext Transform( this IImageProcessingContext source, From 7fc74f37faddc4cc24ea3cb3726a86662597d9a8 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 18:31:42 +0100 Subject: [PATCH 77/99] reference images for filter tests + DRY out repeated code --- .../Processors/Filters/BlackWhiteTest.cs | 7 +-- .../Processors/Filters/BrightnessTest.cs | 6 +-- .../Processors/Filters/ColorBlindnessTest.cs | 6 +-- .../Processors/Filters/ContrastTest.cs | 6 +-- .../Processors/Filters/FilterTest.cs | 14 +++-- .../Processors/Filters/GrayscaleTest.cs | 15 +----- .../Processing/Processors/Filters/HueTest.cs | 6 +-- .../Processors/Filters/InvertTest.cs | 6 +-- .../Processors/Filters/KodachromeTest.cs | 6 +-- .../Processors/Filters/LomographTest.cs | 6 +-- .../Processors/Filters/OpacityTest.cs | 6 +-- .../Processors/Filters/PolaroidTest.cs | 6 +-- .../Processors/Filters/SaturateTest.cs | 6 +-- .../Processors/Filters/SepiaTest.cs | 6 +-- .../TestUtilities/TestUtils.cs | 53 +++++++++++++++++++ tests/Images/External | 2 +- 16 files changed, 74 insertions(+), 83 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs index b0b9aaa492..4174e9b3ab 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyBlackWhiteFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.BlackWhite()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(ctx => ctx.BlackWhite()); } [Theory] @@ -29,6 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyBlackWhiteFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { + // TODO: We need a DRY-refactor on these tests for all Filter tests! using (Image source = provider.GetImage()) using (Image image = source.Clone()) { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index eccf27899a..ced0a3ca72 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -24,11 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects public void ApplyBrightnessFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Brightness(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index 6c51afd003..06519931e8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -31,11 +31,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyColorBlindnessFilter(TestImageProvider provider, ColorBlindness colorBlindness) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.ColorBlindness(colorBlindness)); - image.DebugSave(provider, colorBlindness.ToString()); - } + provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString()); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs index 337b810181..0154f8a674 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs @@ -24,11 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects public void ApplyContrastFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Contrast(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(x => x.Contrast(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index a98153087b..bd66a29eec 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -21,14 +21,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); - Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); - Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); - image.Mutate(x => x.Filter(brightness * hue * saturation)); - image.DebugSave(provider); - } + Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); + Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); + Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); + Matrix4x4 m = brightness * hue * saturation; + + provider.RunValidatingProcessorTest(x => x.Filter(m)); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs index 711c8e10af..1991663916 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs @@ -29,20 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyGrayscaleFilter(TestImageProvider provider, GrayscaleMode value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Grayscale(value)); - var rgb = default(Rgb24); - System.Span span = image.Frames.RootFrame.GetPixelSpan(); - for (int i = 0; i < span.Length; i++) - { - span[i].ToRgb24(ref rgb); - Assert.Equal(rgb.R, rgb.B); - Assert.Equal(rgb.B, rgb.G); - } - - image.DebugSave(provider, value.ToString()); - } + provider.RunValidatingProcessorTest(x => x.Grayscale(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs index 98dd95515d..0cf42a8d57 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs @@ -24,11 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyHueFilter(TestImageProvider provider, int value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Hue(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(x => x.Hue(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs index 69df033f01..b075a059de 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects public void ApplyInvertFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Invert()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(x => x.Invert()); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs index 6daef29aac..f133da34a0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyKodachromeFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Kodachrome()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(x => x.Kodachrome()); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 4e54828a67..08a4e6ff9f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyLomographFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Lomograph()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(x => x.Lomograph()); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs index 9ba77b9836..9f888c09f5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs @@ -24,11 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects public void ApplyAlphaFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Opacity(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(x => x.Opacity(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs index 42bd859dc6..aa5b773cd9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyPolaroidFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Polaroid()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(x => x.Polaroid()); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs index 8cfbb198c8..440564c26f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs @@ -24,11 +24,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplySaturationFilter(TestImageProvider provider, float value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Saturate(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(x => x.Saturate(value), value); } [Theory] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs index 9947d21d05..71b8f311f3 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplySepiaFilter(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Sepia()); - image.DebugSave(provider); - } + provider.RunValidatingProcessorTest(x => x.Sepia()); } [Theory] diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index a468c21165..9050db15cb 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -10,6 +10,8 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests { + using SixLabors.Primitives; + /// /// Various utility and extension methods. /// @@ -142,5 +144,56 @@ namespace SixLabors.ImageSharp.Tests /// /// The pixel types internal static PixelTypes[] GetAllPixelTypes() => (PixelTypes[])Enum.GetValues(typeof(PixelTypes)); + + /// + /// Utility for testing image processor extension methods: + /// 1. Run a processor defined by 'process' + /// 2. Run 'DebugSave()' to save the output locally + /// 3. Run 'CompareToReferenceOutput()' to compare the results to the expected output + /// + internal static void RunValidatingProcessorTest( + this TestImageProvider provider, + Action> process, + object testOutputDetails = null) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(process); + image.DebugSave(provider, testOutputDetails); + image.CompareToReferenceOutput(provider, testOutputDetails); + } + } + + internal static void RunRectangleConstrainedValidatingProcessorTest( + this TestImageProvider provider, + Action, Rectangle> process, + object testOutputDetails = null) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); + image.Mutate(x => process(x, bounds)); + image.DebugSave(provider, testOutputDetails); + image.CompareToReferenceOutput(provider, testOutputDetails); + } + } + + /// + /// Same as but without the 'CompareToReferenceOutput()' step. + /// + internal static void RunProcessorTest( + this TestImageProvider provider, + Action> process, + object testOutputDetails = null) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(process); + image.DebugSave(provider, testOutputDetails); + } + } } } \ No newline at end of file diff --git a/tests/Images/External b/tests/Images/External index dc5479d00b..e12e75f129 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit dc5479d00b2312f691e6249b9f7765e2316d4a30 +Subproject commit e12e75f12925de1b88dc7e564c509ee896aa5f53 From 38a3e38c35cd65e580da3412c182193f243300ef Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 18:40:18 +0100 Subject: [PATCH 78/99] there is no need for separate Apply****InBox tests, having one in FilterTest is sufficient --- .../Processors/Filters/BlackWhiteTest.cs | 20 ------------ .../Processors/Filters/BrightnessTest.cs | 19 ------------ .../Processors/Filters/ColorBlindnessTest.cs | 19 ------------ .../Processors/Filters/ContrastTest.cs | 19 ------------ .../Processors/Filters/FilterTest.cs | 31 +++++++++---------- .../Processors/Filters/GrayscaleTest.cs | 19 ------------ .../Processing/Processors/Filters/HueTest.cs | 19 ------------ .../Processors/Filters/InvertTest.cs | 19 ------------ .../Processors/Filters/KodachromeTest.cs | 19 ------------ .../Processors/Filters/LomographTest.cs | 19 ------------ .../Processors/Filters/OpacityTest.cs | 19 ------------ .../Processors/Filters/PolaroidTest.cs | 19 ------------ .../Processors/Filters/SaturateTest.cs | 19 ------------ .../Processors/Filters/SepiaTest.cs | 19 ------------ .../TestUtilities/TestUtils.cs | 3 ++ tests/Images/External | 2 +- 16 files changed, 18 insertions(+), 266 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs index 4174e9b3ab..e373230a73 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BlackWhiteTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -19,23 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(ctx => ctx.BlackWhite()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplyBlackWhiteFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - // TODO: We need a DRY-refactor on these tests for all Filter tests! - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.BlackWhite(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs index ced0a3ca72..783e55d832 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/BrightnessTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects @@ -26,22 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { provider.RunValidatingProcessorTest(ctx => ctx.Brightness(value), value); } - - [Theory] - [WithTestPatternImages(nameof(BrightnessValues), 48, 48, PixelTypes.Rgba32)] - public void ApplyBrightnessFilterInBox(TestImageProvider provider, float value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Brightness(value, bounds)); - image.DebugSave(provider, value); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs index 06519931e8..c0df24d291 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ColorBlindnessTest.cs @@ -3,9 +3,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -33,22 +31,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.ColorBlindness(colorBlindness), colorBlindness.ToString()); } - - [Theory] - [WithTestPatternImages(nameof(ColorBlindnessFilters), 48, 48, PixelTypes.Rgba32)] - public void ApplyColorBlindnessFilterInBox(TestImageProvider provider, ColorBlindness colorBlindness) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.ColorBlindness(colorBlindness, bounds)); - image.DebugSave(provider, colorBlindness.ToString()); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs index 0154f8a674..b532649b3d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/ContrastTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects @@ -26,22 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { provider.RunValidatingProcessorTest(x => x.Contrast(value), value); } - - [Theory] - [WithTestPatternImages(nameof(ContrastValues), 48, 48, PixelTypes.Rgba32)] - public void ApplyContrastFilterInBox(TestImageProvider provider, float value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Contrast(value, bounds)); - image.DebugSave(provider, value); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index bd66a29eec..515b970aee 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -21,10 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyFilter(TestImageProvider provider) where TPixel : struct, IPixel { - Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); - Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); - Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); - Matrix4x4 m = brightness * hue * saturation; + Matrix4x4 m = CreateCombinedTestFilterMatrix(); provider.RunValidatingProcessorTest(x => x.Filter(m)); } @@ -34,19 +31,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyFilterInBox(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); - - Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); - Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); - Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); - image.Mutate(x => x.Filter(brightness * hue * saturation, bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } + Matrix4x4 m = CreateCombinedTestFilterMatrix(); + + provider.RunRectangleConstrainedValidatingProcessorTest((x, b) => x.Filter(m, b)); } + + private static Matrix4x4 CreateCombinedTestFilterMatrix() + { + Matrix4x4 brightness = MatrixFilters.CreateBrightnessFilter(0.9F); + Matrix4x4 hue = MatrixFilters.CreateHueFilter(180F); + Matrix4x4 saturation = MatrixFilters.CreateSaturateFilter(1.5F); + Matrix4x4 m = brightness * hue * saturation; + return m; + } + } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs index 1991663916..f61c529a38 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/GrayscaleTest.cs @@ -4,9 +4,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -31,22 +29,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Grayscale(value), value); } - - [Theory] - [WithTestPatternImages(nameof(GrayscaleModeTypes), 48, 48, PixelTypes.Rgba32)] - public void ApplyGrayscaleFilterInBox(TestImageProvider provider, GrayscaleMode value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Grayscale(value, bounds)); - image.DebugSave(provider, value.ToString()); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs index 0cf42a8d57..fe9f9d5db2 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/HueTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -26,22 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Hue(value), value); } - - [Theory] - [WithTestPatternImages(nameof(HueValues), 48, 48, PixelTypes.Rgba32)] - public void ApplyHueFilterInBox(TestImageProvider provider, int value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Hue(value, bounds)); - image.DebugSave(provider, value); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs index b075a059de..452397c5eb 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/InvertTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects @@ -19,22 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { provider.RunValidatingProcessorTest(x => x.Invert()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplyInvertFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Invert(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs index f133da34a0..a5165250b7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/KodachromeTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -19,22 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Kodachrome()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplyKodachromeFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Kodachrome(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 08a4e6ff9f..09c8becec3 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -19,22 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Lomograph()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplyLomographFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Lomograph(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs index 9f888c09f5..2cba4fb8ed 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/OpacityTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects @@ -26,22 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Effects { provider.RunValidatingProcessorTest(x => x.Opacity(value), value); } - - [Theory] - [WithTestPatternImages(nameof(AlphaValues), 48, 48, PixelTypes.Rgba32)] - public void ApplyAlphaFilterInBox(TestImageProvider provider, float value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Opacity(value, bounds)); - image.DebugSave(provider, value); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs index aa5b773cd9..4e0347d2a4 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/PolaroidTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -19,22 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Polaroid()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplyPolaroidFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Polaroid(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs index 440564c26f..9b963b94a4 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SaturateTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -26,22 +24,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Saturate(value), value); } - - [Theory] - [WithTestPatternImages(nameof(SaturationValues), 48, 48, PixelTypes.Rgba32)] - public void ApplySaturationFilterInBox(TestImageProvider provider, float value) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Saturate(value, bounds)); - image.DebugSave(provider, value); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs index 71b8f311f3..2a63e992d7 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/SepiaTest.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters @@ -19,22 +17,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { provider.RunValidatingProcessorTest(x => x.Sepia()); } - - [Theory] - [WithTestPatternImages(48, 48, PixelTypes.Rgba32)] - public void ApplySepiaFilterInBox(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image source = provider.GetImage()) - using (Image image = source.Clone()) - { - var bounds = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2); - - image.Mutate(x => x.Sepia(bounds)); - image.DebugSave(provider); - - ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); - } - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 9050db15cb..7ba890e71e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -165,6 +165,9 @@ namespace SixLabors.ImageSharp.Tests } } + /// + /// Same as but with an additional parameter passed to 'process' + /// internal static void RunRectangleConstrainedValidatingProcessorTest( this TestImageProvider provider, Action, Rectangle> process, diff --git a/tests/Images/External b/tests/Images/External index e12e75f129..ddc4045926 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit e12e75f12925de1b88dc7e564c509ee896aa5f53 +Subproject commit ddc4045926bb0331be8c1785d9adf5f76dc4e52e From 7ec95f453465af136c5bc4ee6be8f51a4c4e68af Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 19:20:42 +0100 Subject: [PATCH 79/99] lower default tolerance TolerantImageComparer.DefaultImageThreshold for travis --- .../ImageComparison/ImageComparer.cs | 7 ++++++- .../ImageComparison/TolerantImageComparer.cs | 21 ++++++++++++++++++- .../TestUtilities/TestUtils.cs | 7 +++++-- .../TestUtilities/Tests/ImageComparerTests.cs | 5 ++++- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs index d23ab02028..d656530161 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs @@ -15,8 +15,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison { public static ImageComparer Exact { get; } = Tolerant(0, 0); + /// + /// Returns an instance of . + /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. + /// Negative value for 'imageThreshold' indicates default threshold (see ). + /// public static ImageComparer Tolerant( - float imageThreshold = TolerantImageComparer.DefaultImageThreshold, + float imageThreshold = -1, int perPixelManhattanThreshold = 0) { return new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index d20bd72ac1..db0048558a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -12,10 +12,19 @@ public class TolerantImageComparer : ImageComparer { - public const float DefaultImageThreshold = 1.0f / (100 * 100 * 255); + public static readonly float DefaultImageThreshold = GetDefaultImageThreshold(); + /// + /// Negative value for 'imageThreshold' indicates default threshold (see ). + /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. + /// public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshold = 0) { + if (imageThreshold < 0) + { + imageThreshold = DefaultImageThreshold; + } + this.ImageThreshold = imageThreshold; this.PerPixelManhattanThreshold = perPixelManhattanThreshold; } @@ -107,5 +116,15 @@ [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Diff(byte a, byte b) => Math.Abs(a - b); + + private static float GetDefaultImageThreshold() + { + // 1% of all pixels in a 100*100 pixel area are allowed to have a difference of 1 unit + float t = 1.0f / (100 * 100 * 255); + + // but not with Mono System.Drawing! + // TODO: We may need a runtime-specific check here, instead of the OS specific one! + return TestEnvironment.IsWindows ? t : 4 * t; + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 7ba890e71e..83fa9bc71d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests { + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; /// @@ -154,7 +155,8 @@ namespace SixLabors.ImageSharp.Tests internal static void RunValidatingProcessorTest( this TestImageProvider provider, Action> process, - object testOutputDetails = null) + object testOutputDetails = null, + ImageComparer comparer = null) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -171,7 +173,8 @@ namespace SixLabors.ImageSharp.Tests internal static void RunRectangleConstrainedValidatingProcessorTest( this TestImageProvider provider, Action, Rectangle> process, - object testOutputDetails = null) + object testOutputDetails = null, + ImageComparer comparer = null) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs index f131f51f21..59e6ec5fba 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs @@ -68,7 +68,10 @@ namespace SixLabors.ImageSharp.Tests { using (Image clone = image.Clone()) { - ImagingTestCaseUtility.ModifyPixel(clone, 3, 1, 2); + // Mono System.Drawing is trolling us! See TolerantImageComparer.GetDefaultImageThreshold()! + byte tooBigToSucceed = TestEnvironment.IsWindows ? (byte)2 : (byte)6; + + ImagingTestCaseUtility.ModifyPixel(clone, 3, 1, tooBigToSucceed); var comparer = ImageComparer.Tolerant(); From 50f8f2807e445d84327ba751bcb3d733198d8617 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 21 Jan 2018 19:40:37 +0100 Subject: [PATCH 80/99] Higher threshold for LomographTest --- .../Processing/Processors/Filters/LomographTest.cs | 4 +++- .../TestUtilities/ImageComparison/ImageComparer.cs | 2 +- .../ImageComparison/TolerantImageComparer.cs | 4 ++-- .../TestUtilities/Tests/ImageComparerTests.cs | 13 +++++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 09c8becec3..622d7554b1 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -7,6 +7,8 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + [GroupOutput("Filters")] public class LomographTest { @@ -15,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyLomographFilter(TestImageProvider provider) where TPixel : struct, IPixel { - provider.RunValidatingProcessorTest(x => x.Lomograph()); + provider.RunValidatingProcessorTest(x => x.Lomograph(), comparer: ImageComparer.Tolerant(-2)); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs index d656530161..2820e93990 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison /// /// Returns an instance of . /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. - /// Negative value for 'imageThreshold' indicates default threshold (see ). + /// Negative value for 'imageThreshold' indicates multiplier to be applied to the default threshold (see ). /// public static ImageComparer Tolerant( float imageThreshold = -1, diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index db0048558a..549786d8b9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -15,14 +15,14 @@ public static readonly float DefaultImageThreshold = GetDefaultImageThreshold(); /// - /// Negative value for 'imageThreshold' indicates default threshold (see ). + /// Negative value for 'imageThreshold' indicates a multiplier to be applied to the default threshold (see ). /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. /// public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshold = 0) { if (imageThreshold < 0) { - imageThreshold = DefaultImageThreshold; + imageThreshold *= -DefaultImageThreshold; } this.ImageThreshold = imageThreshold; diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs index 59e6ec5fba..cfc7a10a4a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs @@ -23,6 +23,19 @@ namespace SixLabors.ImageSharp.Tests private ITestOutputHelper Output { get; } + [Fact] + public void TolerantImageComparer_NegativeThresholdIsMultiplierForDefault() + { + var c1 = (TolerantImageComparer)ImageComparer.Tolerant(); + var c2 = (TolerantImageComparer)ImageComparer.Tolerant(-2); + var c3 = (TolerantImageComparer)ImageComparer.Tolerant(-42); + + Assert.True(c1.ImageThreshold > 0); + Assert.Equal(TolerantImageComparer.DefaultImageThreshold, c1.ImageThreshold); + Assert.Equal(c1.ImageThreshold * 2, c2.ImageThreshold); + Assert.Equal(c1.ImageThreshold * 42, c3.ImageThreshold); + } + [Theory] [WithTestPatternImages(100,100,PixelTypes.Rgba32, 0.0001f, 1)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0, 0)] From 1542229540621968c32cea923cb4eb08fb17ed02 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 22 Jan 2018 02:17:05 +0100 Subject: [PATCH 81/99] undo Linux-specific hacking, run `CompareToReferenceOutput()` on Windows only --- .../Processors/Filters/LomographTest.cs | 4 +--- .../ImageComparison/ImageComparer.cs | 3 +-- .../ImageComparison/TolerantImageComparer.cs | 19 +++-------------- .../TestUtilities/TestUtils.cs | 12 ++++++++++- .../TestUtilities/Tests/ImageComparerTests.cs | 21 +++---------------- 5 files changed, 19 insertions(+), 40 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs index 622d7554b1..09c8becec3 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/LomographTest.cs @@ -7,8 +7,6 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters { - using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - [GroupOutput("Filters")] public class LomographTest { @@ -17,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters public void ApplyLomographFilter(TestImageProvider provider) where TPixel : struct, IPixel { - provider.RunValidatingProcessorTest(x => x.Lomograph(), comparer: ImageComparer.Tolerant(-2)); + provider.RunValidatingProcessorTest(x => x.Lomograph()); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs index 2820e93990..920e633795 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs @@ -18,10 +18,9 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison /// /// Returns an instance of . /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. - /// Negative value for 'imageThreshold' indicates multiplier to be applied to the default threshold (see ). /// public static ImageComparer Tolerant( - float imageThreshold = -1, + float imageThreshold = TolerantImageComparer.DefaultImageThreshold, int perPixelManhattanThreshold = 0) { return new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index 549786d8b9..e68a1fbfea 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -12,18 +12,15 @@ public class TolerantImageComparer : ImageComparer { - public static readonly float DefaultImageThreshold = GetDefaultImageThreshold(); + // 1% of all pixels in a 100*100 pixel area are allowed to have a difference of 1 unit + public const float DefaultImageThreshold = 1.0f / (100 * 100 * 255); /// - /// Negative value for 'imageThreshold' indicates a multiplier to be applied to the default threshold (see ). /// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. /// public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshold = 0) { - if (imageThreshold < 0) - { - imageThreshold *= -DefaultImageThreshold; - } + Guard.MustBeGreaterThanOrEqualTo(imageThreshold, 0, nameof(imageThreshold)); this.ImageThreshold = imageThreshold; this.PerPixelManhattanThreshold = perPixelManhattanThreshold; @@ -116,15 +113,5 @@ [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Diff(byte a, byte b) => Math.Abs(a - b); - - private static float GetDefaultImageThreshold() - { - // 1% of all pixels in a 100*100 pixel area are allowed to have a difference of 1 unit - float t = 1.0f / (100 * 100 * 255); - - // but not with Mono System.Drawing! - // TODO: We may need a runtime-specific check here, instead of the OS specific one! - return TestEnvironment.IsWindows ? t : 4 * t; - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 83fa9bc71d..dd033ae7c8 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -146,12 +146,17 @@ namespace SixLabors.ImageSharp.Tests /// The pixel types internal static PixelTypes[] GetAllPixelTypes() => (PixelTypes[])Enum.GetValues(typeof(PixelTypes)); + /// /// Utility for testing image processor extension methods: /// 1. Run a processor defined by 'process' /// 2. Run 'DebugSave()' to save the output locally /// 3. Run 'CompareToReferenceOutput()' to compare the results to the expected output /// + /// The + /// The image processing method to test. (As a delegate) + /// The value to append to the test output. + /// The custom image comparer to use internal static void RunValidatingProcessorTest( this TestImageProvider provider, Action> process, @@ -163,7 +168,12 @@ namespace SixLabors.ImageSharp.Tests { image.Mutate(process); image.DebugSave(provider, testOutputDetails); - image.CompareToReferenceOutput(provider, testOutputDetails); + + // TODO: Investigate the cause of pixel inaccuracies under Linux + if (TestEnvironment.IsWindows) + { + image.CompareToReferenceOutput(provider, testOutputDetails); + } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs index cfc7a10a4a..3f8ec05568 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ImageComparerTests.cs @@ -22,20 +22,7 @@ namespace SixLabors.ImageSharp.Tests } private ITestOutputHelper Output { get; } - - [Fact] - public void TolerantImageComparer_NegativeThresholdIsMultiplierForDefault() - { - var c1 = (TolerantImageComparer)ImageComparer.Tolerant(); - var c2 = (TolerantImageComparer)ImageComparer.Tolerant(-2); - var c3 = (TolerantImageComparer)ImageComparer.Tolerant(-42); - - Assert.True(c1.ImageThreshold > 0); - Assert.Equal(TolerantImageComparer.DefaultImageThreshold, c1.ImageThreshold); - Assert.Equal(c1.ImageThreshold * 2, c2.ImageThreshold); - Assert.Equal(c1.ImageThreshold * 42, c3.ImageThreshold); - } - + [Theory] [WithTestPatternImages(100,100,PixelTypes.Rgba32, 0.0001f, 1)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0, 0)] @@ -81,10 +68,8 @@ namespace SixLabors.ImageSharp.Tests { using (Image clone = image.Clone()) { - // Mono System.Drawing is trolling us! See TolerantImageComparer.GetDefaultImageThreshold()! - byte tooBigToSucceed = TestEnvironment.IsWindows ? (byte)2 : (byte)6; - - ImagingTestCaseUtility.ModifyPixel(clone, 3, 1, tooBigToSucceed); + byte perChannelChange = 2; + ImagingTestCaseUtility.ModifyPixel(clone, 3, 1, perChannelChange); var comparer = ImageComparer.Tolerant(); From 1462e45ce1a87d771e5c71b55e00ca8135f15646 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 00:21:03 +1100 Subject: [PATCH 82/99] Update submodule --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 02687e772f..ddc4045926 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 02687e772f84b1d518ab83eacdfafba476f824e6 +Subproject commit ddc4045926bb0331be8c1785d9adf5f76dc4e52e From 7c7a343fbfede0134c8e4b41d5389ca44856ac3b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 11:12:06 +1100 Subject: [PATCH 83/99] Remove unused files --- .gitignore | 3 + ImageSharp.v2.ncrunchsolution | 14 ---- ImageSharp.v3.ncrunchsolution | 6 -- config.wyam | 4 - packages.xml | 153 ---------------------------------- theme/index.cshtml | 3 - 6 files changed, 3 insertions(+), 180 deletions(-) delete mode 100644 ImageSharp.v2.ncrunchsolution delete mode 100644 ImageSharp.v3.ncrunchsolution delete mode 100644 config.wyam delete mode 100644 packages.xml delete mode 100644 theme/index.cshtml diff --git a/.gitignore b/.gitignore index c2e6f7d536..4942818972 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ bld/ # Visual Studo 2015 cache/options directory .vs/ +# Jetbrains Rider cache/options directory +.idea/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/ImageSharp.v2.ncrunchsolution b/ImageSharp.v2.ncrunchsolution deleted file mode 100644 index b98737f1c0..0000000000 --- a/ImageSharp.v2.ncrunchsolution +++ /dev/null @@ -1,14 +0,0 @@ - - 1 - false - false - true - UseDynamicAnalysis - UseStaticAnalysis - UseStaticAnalysis - UseStaticAnalysis - UseDynamicAnalysis - - - - \ No newline at end of file diff --git a/ImageSharp.v3.ncrunchsolution b/ImageSharp.v3.ncrunchsolution deleted file mode 100644 index 10420ac91d..0000000000 --- a/ImageSharp.v3.ncrunchsolution +++ /dev/null @@ -1,6 +0,0 @@ - - - True - True - - \ No newline at end of file diff --git a/config.wyam b/config.wyam deleted file mode 100644 index 3a4b64c540..0000000000 --- a/config.wyam +++ /dev/null @@ -1,4 +0,0 @@ -#recipe Docs -Settings[Keys.Host] = "imagesharp.org"; -Settings[Keys.Title] = "Image Sharp"; -FileSystem.OutputPath = "./docs"; \ No newline at end of file diff --git a/packages.xml b/packages.xml deleted file mode 100644 index c7ff4de4cd..0000000000 --- a/packages.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/theme/index.cshtml b/theme/index.cshtml deleted file mode 100644 index d3656f800b..0000000000 --- a/theme/index.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -Title: Home ---- -Welcome to the documentation for ImageSharp \ No newline at end of file From e985d6004bc2217241a72faafaabcd35622e5b66 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 12:23:16 +1100 Subject: [PATCH 84/99] Use premultiplication for convolution #428 --- .../Common/Extensions/Vector4Extensions.cs | 26 +++++++++++++++++++ .../Convolution/Convolution2DProcessor.cs | 4 +-- .../Convolution/Convolution2PassProcessor.cs | 4 +-- .../Convolution/ConvolutionProcessor.cs | 4 +-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs index 7cb193e828..64ebeb1f33 100644 --- a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs +++ b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs @@ -12,6 +12,32 @@ namespace SixLabors.ImageSharp /// internal static class Vector4Extensions { + /// + /// Premultiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. + /// + /// The to premultiply + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Premultiply(this Vector4 source) + { + float w = source.W; + Vector4 premultiplied = source * w; + return new Vector4(premultiplied.X, premultiplied.Y, premultiplied.Z, w); + } + + /// + /// Reverses the result of premultiplying a vector via . + /// + /// The to premultiply + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 UnPremultiply(this Vector4 source) + { + float w = source.W; + Vector4 unpremultiplied = source / w; + return new Vector4(unpremultiplied.X, unpremultiplied.Y, unpremultiplied.Z, w); + } + /// /// Compresses a linear color signal to its sRGB equivalent. /// diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs index b85432ac54..cf9b7be9c8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int offsetX = x + fxr; offsetX = offsetX.Clamp(0, maxX); - var currentColor = sourceOffsetRow[offsetX].ToVector4(); + Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); if (fy < kernelXHeight) { @@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors float blue = MathF.Sqrt((bX * bX) + (bY * bY)); ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); + pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); } }); diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs index 362fa5c508..57f434ee08 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs @@ -113,13 +113,13 @@ namespace SixLabors.ImageSharp.Processing.Processors offsetX = offsetX.Clamp(0, maxX); - var currentColor = row[offsetX].ToVector4(); + Vector4 currentColor = row[offsetX].ToVector4().Premultiply(); destination += kernel[fy, fx] * currentColor; } } ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(destination); + pixel.PackFromVector4(destination.UnPremultiply()); } }); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs index c0d3fdcfec..96db9a4ce0 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Processing.Processors offsetX = offsetX.Clamp(0, maxX); - var currentColor = sourceOffsetRow[offsetX].ToVector4(); + Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); currentColor *= this.KernelXY[fy, fx]; red += currentColor.X; @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Processing.Processors } ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); + pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); } }); From 219fdbd36bdc9042739f0175596a5513df1877a8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 16:31:28 +1100 Subject: [PATCH 85/99] Use premultiply for resize. Fix #428 --- .../Processors/Transforms/AffineTransformProcessor.cs | 4 ++-- .../Transforms/ProjectiveTransformProcessor.cs | 4 ++-- .../Processing/Processors/Transforms/WeightsWindow.cs | 8 ++++---- .../Processors/Convolution/DetectEdgesTest.cs | 11 +++-------- .../Processing/Processors/Transforms/ResizeTests.cs | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 07c247d734..9d7056b67d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -204,7 +204,7 @@ namespace SixLabors.ImageSharp.Processing.Processors var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels - var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); + Vector4 mupltiplied = vector.Premultiply(); sum += mupltiplied * xWeight * yWeight; } } @@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Processing.Processors ref TPixel dest = ref destRow[x]; // Reverse the premultiplication - dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); + dest.PackFromVector4(sum.UnPremultiply()); } }); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index f53585093b..463d3717d0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -199,7 +199,7 @@ namespace SixLabors.ImageSharp.Processing.Processors var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels - var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); + Vector4 mupltiplied = vector.Premultiply(); sum += mupltiplied * xWeight * yWeight; } } @@ -207,7 +207,7 @@ namespace SixLabors.ImageSharp.Processing.Processors ref TPixel dest = ref destRow[x]; // Reverse the premultiplication - dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); + dest.PackFromVector4(sum.UnPremultiply()); } }); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs index d23ee5a060..b0a530514e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors { float weight = Unsafe.Add(ref horizontalValues, i); Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v * weight; + result += v.Premultiply() * weight; } return result; @@ -114,10 +114,10 @@ namespace SixLabors.ImageSharp.Processing.Processors { float weight = Unsafe.Add(ref horizontalValues, i); Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v.Expand() * weight; + result += v.Premultiply().Expand() * weight; } - return result; + return result.UnPremultiply(); } /// @@ -144,7 +144,7 @@ namespace SixLabors.ImageSharp.Processing.Processors result += firstPassPixels[x, index] * yw; } - return result; + return result.UnPremultiply(); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index c2d8916384..1b678f20bc 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution public class DetectEdgesTest : FileTestBase { public static readonly string[] CommonTestImages = { TestImages.Png.Bike }; - + public static readonly TheoryData DetectEdgesFilters = new TheoryData { EdgeDetection.Kayyali, @@ -54,9 +54,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { image.Mutate(x => x.DetectEdges()); image.DebugSave(provider); - - // TODO: Enable once we have updated the images - // image.CompareToReferenceOutput(provider); + image.CompareToReferenceOutput(provider); } } @@ -85,10 +83,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution image.DebugSave(provider); // TODO: Enable once we have updated the images - //image.CompareToReferenceOutput(provider); - - // TODO: We don't need this any longer after switching to ReferenceImages - //ImageComparer.Tolerant().EnsureProcessorChangesAreConstrained(source, image, bounds); + image.CompareToReferenceOutput(provider); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 92f1af58ed..72febea340 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, true)); + image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2, KnownResamplers.NearestNeighbor)); // Comparer fights decoder with gif-s. Could not use CompareToReferenceOutput here :( image.DebugSave(provider, extension: Extensions.Gif); From 6039cd7607b1efbd0290098163e8c2cc11b801ed Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 16:35:56 +1100 Subject: [PATCH 86/99] Update reference images --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index ddc4045926..757411f91f 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit ddc4045926bb0331be8c1785d9adf5f76dc4e52e +Subproject commit 757411f91f1164e41a300874655a77ef3b390067 From 76d620d1597c268ecc19f22adf10257c326177bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 16:41:47 +1100 Subject: [PATCH 87/99] Re-enable the reference output for Detect Edges --- .../Processing/Processors/Convolution/DetectEdgesTest.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index 1b678f20bc..323842556e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; @@ -39,9 +38,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { image.Mutate(x => x.DetectEdges(detector)); image.DebugSave(provider, detector.ToString()); - - // TODO: Enable once we have updated the images - // image.CompareToReferenceOutput(provider, detector.ToString()); + image.CompareToReferenceOutput(provider, detector.ToString()); } } @@ -81,8 +78,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution image.Mutate(x => x.DetectEdges(bounds)); image.DebugSave(provider); - - // TODO: Enable once we have updated the images image.CompareToReferenceOutput(provider); } } From d19e7cebf36935221d4d02ba79eb0f43ddd306ad Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 16:47:40 +1100 Subject: [PATCH 88/99] Add kaboom image for future testing --- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/kaboom.png | Bin 0 -> 573925 bytes 2 files changed, 1 insertion(+) create mode 100644 tests/Images/Input/Png/kaboom.png diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c542fa8808..864c963327 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -35,6 +35,7 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb48BppInterlaced = "Png/rgb-48bpp-interlaced.png"; public const string SnakeGame = "Png/SnakeGame.png"; public const string Icon = "Png/icon.png"; + public const string Kaboom = "Png/kaboom.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/kaboom.png b/tests/Images/Input/Png/kaboom.png new file mode 100644 index 0000000000000000000000000000000000000000..c2abdf465d481bb5e82011c53080b5719f4b3695 GIT binary patch literal 573925 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

M&I3x&a<;6QeLG5XeIMa-yvq%A$?>_+4 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 z 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 mAyp3#XWvGj6`58E|=l(D!LEdKu~?t89~fn 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 YbRb`HB%pC`*n>Aa6m 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≪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Š$>I5w%cv+nv8ZHW&);M722P|rOZxu_(turP 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ν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 Date: Tue, 23 Jan 2018 17:19:28 +1100 Subject: [PATCH 89/99] Add new test for kaboom --- .../Processing/Processors/Transforms/ResizeTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 72febea340..8370a802cd 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -82,6 +82,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + [Theory] + [WithFile(TestImages.Png.Kaboom, DefaultPixelType)] + public void Resize_DoesNotBleedAlphaPixels(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2)); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + } + [Theory] [WithFile(TestImages.Gif.Giphy, DefaultPixelType)] public void Resize_IsAppliedToAllFrames(TestImageProvider provider) From 61d3883d48d312797f5ca5a28c8f6a03aec3b4bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 23 Jan 2018 17:20:09 +1100 Subject: [PATCH 90/99] Update reference images --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index 757411f91f..376605e05b 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 757411f91f1164e41a300874655a77ef3b390067 +Subproject commit 376605e05bb704d425b2d17bf5b310f5376da22e From 1dae3bd1016d729c8378bf7d5a18744567931042 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 24 Jan 2018 12:06:43 +1100 Subject: [PATCH 91/99] Drop an unnecessary constructor invocation for perf --- src/ImageSharp/Common/Extensions/Vector4Extensions.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs index 64ebeb1f33..8133ebb38e 100644 --- a/src/ImageSharp/Common/Extensions/Vector4Extensions.cs +++ b/src/ImageSharp/Common/Extensions/Vector4Extensions.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp internal static class Vector4Extensions { ///

- /// Premultiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. + /// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. /// /// The to premultiply /// The @@ -22,7 +22,8 @@ namespace SixLabors.ImageSharp { float w = source.W; Vector4 premultiplied = source * w; - return new Vector4(premultiplied.X, premultiplied.Y, premultiplied.Z, w); + premultiplied.W = w; + return premultiplied; } /// @@ -35,7 +36,8 @@ namespace SixLabors.ImageSharp { float w = source.W; Vector4 unpremultiplied = source / w; - return new Vector4(unpremultiplied.X, unpremultiplied.Y, unpremultiplied.Z, w); + unpremultiplied.W = w; + return unpremultiplied; } /// From d2df29e4b43682d786b9cf04ee087f1bbcfc992d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 24 Jan 2018 16:17:19 +1100 Subject: [PATCH 92/99] Standardize EXIF dimension transform updates --- .../DefaultInternalImageProcessorContext.cs | 4 +-- .../Processors/CloningImageProcessor.cs | 6 ++-- .../Transforms/AffineTransformProcessor.cs | 2 +- .../Processors/Transforms/CropProcessor.cs | 4 +++ .../InterpolatedTransformProcessorBase.cs | 24 +------------ .../ProjectiveTransformProcessor.cs | 2 +- .../Transforms/ResamplingWeightedProcessor.cs | 2 +- .../Transforms/TransformProcessorBase.cs | 20 +++++++++++ .../Processing/Transforms/TransformHelpers.cs | 34 +++++++++++++++++++ 9 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs diff --git a/src/ImageSharp/DefaultInternalImageProcessorContext.cs b/src/ImageSharp/DefaultInternalImageProcessorContext.cs index 575525a773..6e6feed84e 100644 --- a/src/ImageSharp/DefaultInternalImageProcessorContext.cs +++ b/src/ImageSharp/DefaultInternalImageProcessorContext.cs @@ -52,8 +52,8 @@ namespace SixLabors.ImageSharp if (!this.mutate && this.destination == null) { // This will only work if the first processor applied is the cloning one thus - // realistically for this optermissation to work the resize must the first processor - // applied any only up processors will take the douple data path. + // realistically for this optimization to work the resize must the first processor + // applied any only up processors will take the double data path. if (processor is ICloningImageProcessor cloningImageProcessor) { this.destination = cloningImageProcessor.CloneAndApply(this.source, rectangle); diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs index fdee21ed6a..4672b2ad45 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs +++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Processing if (clone.Frames.Count != source.Frames.Count) { - throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. The processor changed the number of frames."); + throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. The processor changed the number of frames."); } Configuration configuration = source.GetConfiguration(); @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing // we now need to move the pixel data/size data from one image base to another if (cloned.Frames.Count != source.Frames.Count) { - throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. The processor changed the number of frames."); + throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. The processor changed the number of frames."); } source.SwapPixelsBuffers(cloned); @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Processing } /// - /// Generates a deep clone of the source image that operatinos should be applied to. + /// Generates a deep clone of the source image that operations should be applied to. /// /// The source image. Cannot be null. /// The source rectangle. diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 9d7056b67d..8595e86922 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) : base(sampler) { - // Tansforms are inverted else the output is the opposite of the expected. + // Transforms are inverted else the output is the opposite of the expected. Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; this.targetDimensions = targetDimensions; diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs index 2657daaa8a..00547d0147 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs @@ -60,5 +60,9 @@ namespace SixLabors.ImageSharp.Processing.Processors source.SwapPixelsBuffers(targetPixels); } } + + /// + protected override void AfterImageApply(Image source, Rectangle sourceRectangle) + => TransformHelpers.UpdateDimensionalMetData(source); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs index 5c32f044a2..27f9a1ace6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs @@ -3,9 +3,7 @@ using System; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors { @@ -13,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The base class for performing interpolated affine and non-affine transforms. /// /// The pixel format. - internal abstract class InterpolatedTransformProcessorBase : CloningImageProcessor + internal abstract class InterpolatedTransformProcessorBase : TransformProcessorBase where TPixel : struct, IPixel { /// @@ -115,25 +113,5 @@ namespace SixLabors.ImageSharp.Processing.Processors return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); } - - /// - protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) - { - ExifProfile profile = destination.MetaData.ExifProfile; - if (profile == null) - { - return; - } - - if (profile.GetValue(ExifTag.PixelXDimension) != null) - { - profile.SetValue(ExifTag.PixelXDimension, destination.Width); - } - - if (profile.GetValue(ExifTag.PixelYDimension) != null) - { - profile.SetValue(ExifTag.PixelYDimension, destination.Height); - } - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 463d3717d0..7e547727e6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) : base(sampler) { - // Tansforms are inverted else the output is the opposite of the expected. + // Transforms are inverted else the output is the opposite of the expected. Matrix4x4.Invert(matrix, out matrix); this.TransformMatrix = matrix; this.targetRectangle = rectangle; diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs index cb65559daa..b9cb58707c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Adapted from /// /// The pixel format. - internal abstract class ResamplingWeightedProcessor : CloningImageProcessor + internal abstract class ResamplingWeightedProcessor : TransformProcessorBase where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs new file mode 100644 index 0000000000..7403a400e7 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// The base class for all transform processors. Any processor that changes the dimensions of the image should inherit from this. + /// + /// The pixel format. + internal abstract class TransformProcessorBase : CloningImageProcessor + where TPixel : struct, IPixel + { + /// + protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) + => TransformHelpers.UpdateDimensionalMetData(destination); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs index 119fc9eeda..74cbc59d27 100644 --- a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs @@ -3,6 +3,8 @@ using System; using System.Numerics; +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; namespace SixLabors.ImageSharp @@ -12,6 +14,38 @@ namespace SixLabors.ImageSharp /// internal class TransformHelpers { + /// + /// Updates the dimensional metadata of a transformed image + /// + /// The pixel format. + /// The image to update + public static void UpdateDimensionalMetData(Image image) + where TPixel : struct, IPixel + { + ExifProfile profile = image.MetaData.ExifProfile; + if (profile == null) + { + return; + } + + // Removing the previously stored value allows us to set a value with our own data tag if required. + if (profile.GetValue(ExifTag.PixelXDimension) != null) + { + profile.RemoveValue(ExifTag.PixelXDimension); + profile.SetValue( + ExifTag.PixelXDimension, + image.Width <= ushort.MaxValue ? (ushort)image.Width : (uint)image.Width); + } + + if (profile.GetValue(ExifTag.PixelYDimension) != null) + { + profile.RemoveValue(ExifTag.PixelYDimension); + profile.SetValue( + ExifTag.PixelYDimension, + image.Height <= ushort.MaxValue ? (ushort)image.Height : (uint)image.Height); + } + } + /// /// Returns the bounding relative to the source for the given transformation matrix. /// From 2cb370f79e6f24e0f782ada3f794d5e04913adfa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 24 Jan 2018 18:35:43 +1100 Subject: [PATCH 93/99] fix ternary casting --- .../Processing/Transforms/TransformHelpers.cs | 24 +++++++++---- .../Transforms/TransformsHelpersTest.cs | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs diff --git a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs index 74cbc59d27..bfb06c4707 100644 --- a/src/ImageSharp/Processing/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Transforms/TransformHelpers.cs @@ -32,17 +32,29 @@ namespace SixLabors.ImageSharp if (profile.GetValue(ExifTag.PixelXDimension) != null) { profile.RemoveValue(ExifTag.PixelXDimension); - profile.SetValue( - ExifTag.PixelXDimension, - image.Width <= ushort.MaxValue ? (ushort)image.Width : (uint)image.Width); + + if (image.Width <= ushort.MaxValue) + { + profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width); + } + else + { + profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width); + } } if (profile.GetValue(ExifTag.PixelYDimension) != null) { profile.RemoveValue(ExifTag.PixelYDimension); - profile.SetValue( - ExifTag.PixelYDimension, - image.Height <= ushort.MaxValue ? (ushort)image.Height : (uint)image.Height); + + if (image.Height <= ushort.MaxValue) + { + profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); + } + else + { + profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs new file mode 100644 index 0000000000..c5b6b1ad72 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public class TransformsHelpersTest + { + [Fact] + public void HelperCanChangeExifDataType() + { + int xy = 1; + + using (var img = new Image(xy, xy)) + { + var profile = new ExifProfile(); + img.MetaData.ExifProfile = profile; + profile.SetValue(ExifTag.PixelXDimension, (uint)xy); + profile.SetValue(ExifTag.PixelYDimension, (uint)xy); + + Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType); + Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType); + + TransformHelpers.UpdateDimensionalMetData(img); + + Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType); + Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType); + } + } + } +} \ No newline at end of file From f773e005a91ce133b1a78ea6f044ee19d2b88325 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 25 Jan 2018 11:53:12 +1100 Subject: [PATCH 94/99] Add missing pixel formats --- .../PixelFormats/PackedPixelConverterHelper.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/PixelFormats/PackedPixelConverterHelper.cs b/src/ImageSharp/PixelFormats/PackedPixelConverterHelper.cs index 0537ff514e..ae5f785a96 100644 --- a/src/ImageSharp/PixelFormats/PackedPixelConverterHelper.cs +++ b/src/ImageSharp/PixelFormats/PackedPixelConverterHelper.cs @@ -290,9 +290,11 @@ namespace SixLabors.ImageSharp.PixelFormats /// The private static bool IsStandardNormalizedType(Type type) { - return type == typeof(Rgba32) + return + type == typeof(Alpha8) || type == typeof(Argb32) - || type == typeof(Alpha8) + || type == typeof(Bgr24) + || type == typeof(Bgra32) || type == typeof(Bgr565) || type == typeof(Bgra4444) || type == typeof(Bgra5551) @@ -300,8 +302,10 @@ namespace SixLabors.ImageSharp.PixelFormats || type == typeof(HalfVector2) || type == typeof(HalfVector4) || type == typeof(Rg32) - || type == typeof(Rgba1010102) - || type == typeof(Rgba64); + || type == typeof(Rgb24) + || type == typeof(Rgba32) + || type == typeof(Rgba64) + || type == typeof(Rgba1010102); } /// From eba1e91fa1b219e1330cc6671d74e9e301b1baaa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 25 Jan 2018 13:45:17 +1100 Subject: [PATCH 95/99] Add simple clone test --- .../ImageSharp.Tests/Image/ImageCloneTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/ImageSharp.Tests/Image/ImageCloneTests.cs diff --git a/tests/ImageSharp.Tests/Image/ImageCloneTests.cs b/tests/ImageSharp.Tests/Image/ImageCloneTests.cs new file mode 100644 index 0000000000..12e0fc8834 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageCloneTests.cs @@ -0,0 +1,36 @@ +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class ImageCloneTests + { + [Theory] + [WithTestPatternImages(9, 9, PixelTypes.Rgba32)] + public void CloneAs_ToBgra32(TestImageProvider provider) + { + using (Image image = provider.GetImage()) + using (Image clone = image.CloneAs()) + { + for (int y = 0; y < image.Height; y++) + { + Span row = image.GetPixelRowSpan(y); + Span rowClone = clone.GetPixelRowSpan(y); + + for (int x = 0; x < image.Width; x++) + { + Rgba32 rgba = row[x]; + Bgra32 bgra = rowClone[x]; + + Assert.Equal(rgba.R, bgra.R); + Assert.Equal(rgba.G, bgra.G); + Assert.Equal(rgba.B, bgra.B); + Assert.Equal(rgba.A, bgra.A); + } + } + } + } + } +} \ No newline at end of file From c13cc85b6878706ed6511006d1396f912b8ce566 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 31 Jan 2018 12:33:49 +1100 Subject: [PATCH 96/99] Fix and optimize error diffusion --- .../Dithering/ErrorDiffusion/ErrorDiffuserBase.cs | 8 +++----- src/ImageSharp/Formats/Png/PngEncoder.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- src/ImageSharp/Quantizers/QuantizerBase{TPixel}.cs | 2 +- src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs | 4 ++-- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs index 510a097eaf..46bafcc0cf 100644 --- a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs +++ b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs @@ -58,7 +58,7 @@ namespace SixLabors.ImageSharp.Dithering.Base this.startingOffset = 0; for (int i = 0; i < this.matrixWidth; i++) { - // Good to disable here as we are not comparing matematical output. + // Good to disable here as we are not comparing mathematical output. // ReSharper disable once CompareOfFloatsByEqualityOperator if (matrix[0, i] != 0) { @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Dithering.Base // Calculate the error Vector4 error = source.ToVector4() - transformed.ToVector4(); - // Loop through and distribute the error amongst neighbouring pixels. + // Loop through and distribute the error amongst neighboring pixels. for (int row = 0; row < this.matrixHeight; row++) { int matrixY = y + row; @@ -115,10 +115,8 @@ namespace SixLabors.ImageSharp.Dithering.Base ref TPixel pixel = ref rowSpan[matrixX]; var offsetColor = pixel.ToVector4(); - var coefficientVector = new Vector4(coefficient); - Vector4 result = ((error * coefficientVector) / this.divisorVector) + offsetColor; - result.W = offsetColor.W; + Vector4 result = ((error * coefficient) / this.divisorVector) + offsetColor; pixel.PackFromVector4(result); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 2fc6911f0b..4abafc9e80 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Png public bool IgnoreMetadata { get; set; } /// - /// Gets or sets the size of the color palette to use. Set to zero to leav png encoding to use pixel data. + /// Gets or sets the size of the color palette to use. Set to zero to leave png encoding to use pixel data. /// public int PaletteSize { get; set; } = 0; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 0efd46ec74..6dbf2eeb80 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Initializes a new instance of the class. /// - /// The options for influancing the encoder + /// The options for influencing the encoder public PngEncoderCore(IPngEncoderOptions options) { this.ignoreMetadata = options.IgnoreMetadata; diff --git a/src/ImageSharp/Quantizers/QuantizerBase{TPixel}.cs b/src/ImageSharp/Quantizers/QuantizerBase{TPixel}.cs index d57865c973..20ba2e637e 100644 --- a/src/ImageSharp/Quantizers/QuantizerBase{TPixel}.cs +++ b/src/ImageSharp/Quantizers/QuantizerBase{TPixel}.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Quantizers.Base } /// - public bool Dither { get; set; } = false; + public bool Dither { get; set; } = true; /// public IErrorDiffuser DitherType { get; set; } = new FloydSteinbergDiffuser(); diff --git a/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs b/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs index cb9eb9b0e3..f08114574e 100644 --- a/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs @@ -281,7 +281,7 @@ namespace SixLabors.ImageSharp.Quantizers } /// - /// Gets the index index of the given color in the palette. + /// Gets the index of the given color in the palette. /// /// The red value. /// The green value. @@ -827,7 +827,7 @@ namespace SixLabors.ImageSharp.Quantizers { if (this.Dither) { - // The colors have changed so we need to use Euclidean distance caclulation to find the closest value. + // The colors have changed so we need to use Euclidean distance calculation to find the closest value. // This palette can never be null here. return this.GetClosestPixel(pixel, this.palette, this.colorMap); } From b0a5fa6ae76caa5c031374bf479b2fc98f0e69ed Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 31 Jan 2018 13:45:09 +1100 Subject: [PATCH 97/99] Add test --- .../Quantization/QuantizedImageTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index a0b14b09ba..b5a8d1265c 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -7,6 +7,18 @@ public class QuantizedImageTests { + [Fact] + public void QuantizersDitherByDefault() + { + var palette = new PaletteQuantizer(); + var octree = new OctreeQuantizer(); + var wu = new WuQuantizer(); + + Assert.True(palette.Dither); + Assert.True(octree.Dither); + Assert.True(wu.Dither); + } + [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, true)] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, false)] From 96b285deedb17e0d41da77086f66a178460a2728 Mon Sep 17 00:00:00 2001 From: Dirk Lemstra Date: Sat, 3 Feb 2018 17:57:14 +0100 Subject: [PATCH 98/99] Move swap to classes to avoid odd issues on Mono. --- .../Common/Extensions/ComparableExtensions.cs | 14 -------------- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 12 +++++++++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +++-- src/ImageSharp/Image/ImageFrame{TPixel}.cs | 4 +++- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Common/Extensions/ComparableExtensions.cs b/src/ImageSharp/Common/Extensions/ComparableExtensions.cs index 8bebb3de79..d6dade7703 100644 --- a/src/ImageSharp/Common/Extensions/ComparableExtensions.cs +++ b/src/ImageSharp/Common/Extensions/ComparableExtensions.cs @@ -169,19 +169,5 @@ namespace SixLabors.ImageSharp { return (byte)value.Clamp(0, 255); } - - /// - /// Swaps the references to two objects in memory. - /// - /// The first reference. - /// The second reference. - /// The type of object. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Swap(ref T first, ref T second) - { - T temp = second; - second = first; - first = temp; - } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5cdf80289c..e39187e086 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -14,7 +14,6 @@ using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; -using static SixLabors.ImageSharp.ComparableExtensions; namespace SixLabors.ImageSharp.Formats.Png { @@ -589,7 +588,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.ProcessDefilteredScanline(this.scanline.Array, image); - Swap(ref this.scanline, ref this.previousScanline); + this.SwapBuffers(); this.currentRow++; } } @@ -665,7 +664,7 @@ namespace SixLabors.ImageSharp.Formats.Png Span rowSpan = image.GetPixelRowSpan(this.currentRow); this.ProcessInterlacedDefilteredScanline(this.scanline.Array, rowSpan, Adam7FirstColumn[this.pass], Adam7ColumnIncrement[this.pass]); - Swap(ref this.scanline, ref this.previousScanline); + this.SwapBuffers(); this.currentRow += Adam7RowIncrement[this.pass]; } @@ -1348,5 +1347,12 @@ namespace SixLabors.ImageSharp.Formats.Png default: throw new ArgumentException($"Not a valid pass index: {passIndex}"); } } + + private void SwapBuffers() + { + Buffer temp = this.previousScanline; + this.previousScanline = this.scanline; + this.scanline = temp; + } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 6dbf2eeb80..385d40b6ba 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -11,7 +11,6 @@ using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Quantizers; -using static SixLabors.ImageSharp.ComparableExtensions; namespace SixLabors.ImageSharp.Formats.Png { @@ -645,7 +644,9 @@ namespace SixLabors.ImageSharp.Formats.Png Buffer r = this.EncodePixelRow(pixels.GetPixelRowSpan(y), y); deflateStream.Write(r.Array, 0, resultLength); - Swap(ref this.rawScanline, ref this.previousScanline); + Buffer temp = this.rawScanline; + this.rawScanline = this.previousScanline; + this.previousScanline = temp; } } diff --git a/src/ImageSharp/Image/ImageFrame{TPixel}.cs b/src/ImageSharp/Image/ImageFrame{TPixel}.cs index e39cc1ab2f..ba475f9cf3 100644 --- a/src/ImageSharp/Image/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/Image/ImageFrame{TPixel}.cs @@ -168,7 +168,9 @@ namespace SixLabors.ImageSharp { Guard.NotNull(pixelSource, nameof(pixelSource)); - ComparableExtensions.Swap(ref this.pixelBuffer, ref pixelSource.pixelBuffer); + Buffer2D temp = this.pixelBuffer; + this.pixelBuffer = pixelSource.pixelBuffer; + pixelSource.pixelBuffer = temp; } /// From 6f6d9e78e6b75203f5bf3f586a85f7c8329fa519 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 9 Feb 2018 14:46:44 +1100 Subject: [PATCH 99/99] Fix #453 --- .../Processors/Transforms/ResizeProcessor.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index 0d8d0d9117..b05d77868f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -191,25 +191,5 @@ namespace SixLabors.ImageSharp.Processing.Processors }); } } - - /// - protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) - { - ExifProfile profile = destination.MetaData.ExifProfile; - if (profile == null) - { - return; - } - - if (profile.GetValue(ExifTag.PixelXDimension) != null) - { - profile.SetValue(ExifTag.PixelXDimension, destination.Width); - } - - if (profile.GetValue(ExifTag.PixelYDimension) != null) - { - profile.SetValue(ExifTag.PixelYDimension, destination.Height); - } - } } } \ No newline at end of file