From b798d8dc014a3fbb6b074a11d2137ba00404a754 Mon Sep 17 00:00:00 2001 From: Devedse Date: Wed, 31 Oct 2018 00:41:07 +0100 Subject: [PATCH 01/66] Work in progress preserving isTrans data --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 97 ++++++++------------ src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 + src/ImageSharp/Formats/Png/PngMetaData.cs | 27 ++++++ src/ImageSharp/MetaData/ImageMetaData.cs | 1 + 4 files changed, 73 insertions(+), 57 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 11c4d831b..024bd6221 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -124,31 +124,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// private PngColorType pngColorType; - /// - /// Represents any color in an 8 bit Rgb24 encoded png that should be transparent - /// - private Rgb24 rgb24Trans; - - /// - /// Represents any color in a 16 bit Rgb24 encoded png that should be transparent - /// - private Rgb48 rgb48Trans; - - /// - /// Represents any color in an 8 bit grayscale encoded png that should be transparent - /// - private byte luminanceTrans; - - /// - /// Represents any color in a 16 bit grayscale encoded png that should be transparent - /// - private ushort luminance16Trans; - - /// - /// Whether the image has transparency chunk and markers were decoded - /// - private bool hasTrans; - /// /// The next chunk of data to return /// @@ -213,7 +188,7 @@ namespace SixLabors.ImageSharp.Formats.Png using (var deframeStream = new ZlibInflateStream(this.currentStream, this.ReadNextDataChunk)) { deframeStream.AllocateNewBytes(chunk.Length); - this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame); + this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame, pngMetaData); } break; @@ -226,7 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Png byte[] alpha = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha); + this.AssignTransparentMarkers(alpha, pngMetaData); break; case PngChunkType.Text: this.ReadTextChunk(metaData, chunk.Data.Array.AsSpan(0, chunk.Length)); @@ -331,7 +306,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset) - => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); + { + return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); + } /// /// Attempts to convert a byte array to a new array where each value in the original array is represented by the @@ -496,16 +473,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The containing data. /// The pixel data. - private void ReadScanlines(Stream dataStream, ImageFrame image) + /// The png meta data + private void ReadScanlines(Stream dataStream, ImageFrame image, PngMetaData pngMetaData) where TPixel : struct, IPixel { if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) { - this.DecodeInterlacedPixelData(dataStream, image); + this.DecodeInterlacedPixelData(dataStream, image, pngMetaData); } else { - this.DecodePixelData(dataStream, image); + this.DecodePixelData(dataStream, image, pngMetaData); } } @@ -515,7 +493,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The compressed pixel data stream. /// The image to decode to. - private void DecodePixelData(Stream compressedStream, ImageFrame image) + /// The png meta data + private void DecodePixelData(Stream compressedStream, ImageFrame image, PngMetaData pngMetaData) where TPixel : struct, IPixel { while (this.currentRow < this.header.Height) @@ -555,7 +534,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("Unknown filter type."); } - this.ProcessDefilteredScanline(scanlineSpan, image); + this.ProcessDefilteredScanline(scanlineSpan, image, pngMetaData); this.SwapBuffers(); this.currentRow++; @@ -569,7 +548,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The compressed pixel data stream. /// The current image. - private void DecodeInterlacedPixelData(Stream compressedStream, ImageFrame image) + /// The png meta data + private void DecodeInterlacedPixelData(Stream compressedStream, ImageFrame image, PngMetaData pngMetaData) where TPixel : struct, IPixel { while (true) @@ -626,7 +606,7 @@ namespace SixLabors.ImageSharp.Formats.Png } Span rowSpan = image.GetPixelRowSpan(this.currentRow); - this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]); + this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetaData, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]); this.SwapBuffers(); @@ -654,7 +634,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The de-filtered scanline /// The image - private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels) + /// The png meta data + private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetaData pngMetaData) where TPixel : struct, IPixel { Span rowSpan = pixels.GetPixelRowSpan(this.currentRow); @@ -674,9 +655,9 @@ namespace SixLabors.ImageSharp.Formats.Png this.header, scanlineSpan, rowSpan, - this.hasTrans, - this.luminance16Trans, - this.luminanceTrans); + pngMetaData.HasTrans, + pngMetaData.Luminance16Trans, + pngMetaData.LuminanceTrans); break; @@ -708,9 +689,9 @@ namespace SixLabors.ImageSharp.Formats.Png rowSpan, this.bytesPerPixel, this.bytesPerSample, - this.hasTrans, - this.rgb48Trans, - this.rgb24Trans); + pngMetaData.HasTrans, + pngMetaData.Rgb48Trans, + pngMetaData.Rgb24Trans); break; @@ -735,9 +716,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The de-filtered scanline /// The current image row. + /// The png meta data /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. - private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, int pixelOffset = 0, int increment = 1) + private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, PngMetaData pngMetaData, int pixelOffset = 0, int increment = 1) where TPixel : struct, IPixel { // Trim the first marker byte from the buffer @@ -757,9 +739,9 @@ namespace SixLabors.ImageSharp.Formats.Png rowSpan, pixelOffset, increment, - this.hasTrans, - this.luminance16Trans, - this.luminanceTrans); + pngMetaData.HasTrans, + pngMetaData.Luminance16Trans, + pngMetaData.LuminanceTrans); break; @@ -796,9 +778,9 @@ namespace SixLabors.ImageSharp.Formats.Png increment, this.bytesPerPixel, this.bytesPerSample, - this.hasTrans, - this.rgb48Trans, - this.rgb24Trans); + pngMetaData.HasTrans, + pngMetaData.Rgb48Trans, + pngMetaData.Rgb24Trans); break; @@ -822,7 +804,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// Decodes and assigns marker colors that identify transparent pixels in non indexed images /// /// The alpha tRNS array - private void AssignTransparentMarkers(ReadOnlySpan alpha) + /// The png meta data + private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetaData pngMetaData) { if (this.pngColorType == PngColorType.Rgb) { @@ -834,16 +817,16 @@ namespace SixLabors.ImageSharp.Formats.Png ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); - this.rgb48Trans = new Rgb48(rc, gc, bc); - this.hasTrans = true; + pngMetaData.Rgb48Trans = new Rgb48(rc, gc, bc); + pngMetaData.HasTrans = true; return; } byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); - this.rgb24Trans = new Rgb24(r, g, b); - this.hasTrans = true; + pngMetaData.Rgb24Trans = new Rgb24(r, g, b); + pngMetaData.HasTrans = true; } } else if (this.pngColorType == PngColorType.Grayscale) @@ -852,14 +835,14 @@ namespace SixLabors.ImageSharp.Formats.Png { if (this.header.BitDepth == 16) { - this.luminance16Trans = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)); + pngMetaData.Luminance16Trans = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)); } else { - this.luminanceTrans = ReadByteLittleEndian(alpha, 0); + pngMetaData.LuminanceTrans = ReadByteLittleEndian(alpha, 0); } - this.hasTrans = true; + pngMetaData.HasTrans = true; } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 7ae716aa0..411d5d69b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -290,6 +290,11 @@ namespace SixLabors.ImageSharp.Formats.Png this.WritePaletteChunk(stream, quantized); } + if (pngMetaData.HasTrans) + { + //Write transparency header + } + this.WritePhysicalChunk(stream, metaData); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metaData); diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 9c7676514..0014defbb 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Png { /// @@ -42,6 +44,31 @@ namespace SixLabors.ImageSharp.Formats.Png /// public float Gamma { get; set; } + /// + /// Gets or sets the Rgb 24 transparent color. This represents any color in an 8 bit Rgb24 encoded png that should be transparent + /// + public Rgb24 Rgb24Trans { get; set; } + + /// + /// Gets or sets the Rgb 48 transparent color. This represents any color in a 16 bit Rgb24 encoded png that should be transparent + /// + public Rgb48 Rgb48Trans { get; set; } + + /// + /// Gets or sets the 8 bit grayscale transparent color. This represents any color in an 8 bit grayscale encoded png that should be transparent + /// + public byte LuminanceTrans { get; set; } + + /// + /// Gets or sets the 16 bit grayscale transparent color. This represents any color in a 16 bit grayscale encoded png that should be transparent + /// + public ushort Luminance16Trans { get; set; } + + /// + /// Gets or sets a value indicating whether the image has transparency chunk and markers were decoded + /// + public bool HasTrans { get; set; } + /// public IDeepCloneable DeepClone() => new PngMetaData(this); } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 73549d98a..ec9037479 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.MetaData { From e65fc5481f1617de7076f3ced7b54803633e772a Mon Sep 17 00:00:00 2001 From: Devedse Date: Wed, 31 Oct 2018 01:14:09 +0100 Subject: [PATCH 02/66] This should write transparency --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 16 +++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 56 ++++++++++++++++++-- src/ImageSharp/Formats/Png/PngMetaData.cs | 8 +-- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 024bd6221..87c22a2ad 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -656,8 +656,8 @@ namespace SixLabors.ImageSharp.Formats.Png scanlineSpan, rowSpan, pngMetaData.HasTrans, - pngMetaData.Luminance16Trans, - pngMetaData.LuminanceTrans); + pngMetaData.Luminance16Trans.GetValueOrDefault(), + pngMetaData.LuminanceTrans.GetValueOrDefault()); break; @@ -690,8 +690,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerPixel, this.bytesPerSample, pngMetaData.HasTrans, - pngMetaData.Rgb48Trans, - pngMetaData.Rgb24Trans); + pngMetaData.Rgb48Trans.GetValueOrDefault(), + pngMetaData.Rgb24Trans.GetValueOrDefault()); break; @@ -740,8 +740,8 @@ namespace SixLabors.ImageSharp.Formats.Png pixelOffset, increment, pngMetaData.HasTrans, - pngMetaData.Luminance16Trans, - pngMetaData.LuminanceTrans); + pngMetaData.Luminance16Trans.GetValueOrDefault(), + pngMetaData.LuminanceTrans.GetValueOrDefault()); break; @@ -779,8 +779,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerPixel, this.bytesPerSample, pngMetaData.HasTrans, - pngMetaData.Rgb48Trans, - pngMetaData.Rgb24Trans); + pngMetaData.Rgb48Trans.GetValueOrDefault(), + pngMetaData.Rgb24Trans.GetValueOrDefault()); break; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 411d5d69b..c76dc308b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -292,7 +292,7 @@ namespace SixLabors.ImageSharp.Formats.Png if (pngMetaData.HasTrans) { - //Write transparency header + this.WriteTransparencyMarkers(stream, pngMetaData); } this.WritePhysicalChunk(stream, metaData); @@ -305,6 +305,50 @@ namespace SixLabors.ImageSharp.Formats.Png quantized?.Dispose(); } + /// + /// Writes the transparency markers to the stream + /// + /// The containing image data. + /// The image meta data. + private void WriteTransparencyMarkers(Stream stream, PngMetaData pngMetaData) + { + if (pngMetaData.ColorType == PngColorType.Rgb) + { + if (pngMetaData.Rgb48Trans != null) + { + var r = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); + var g = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); + var B = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.B); + + var alphaArray = r.Concat(g).Concat(B).ToArray(); + + this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); + } + else if (pngMetaData.Rgb24Trans != null) + { + var alphaArray = new byte[6]; + alphaArray[0] = pngMetaData.Rgb24Trans.Value.R; + alphaArray[2] = pngMetaData.Rgb24Trans.Value.G; + alphaArray[4] = pngMetaData.Rgb24Trans.Value.B; + this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); + } + } + else if (pngMetaData.ColorType == PngColorType.Grayscale) + { + if (pngMetaData.Luminance16Trans != null) + { + var alphaArray = BitConverter.GetBytes(pngMetaData.Luminance16Trans.Value); + + this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); + } + else if (pngMetaData.LuminanceTrans != null) + { + var alphaArray = new byte[2]; + alphaArray[0] = pngMetaData.LuminanceTrans.Value; + } + } + } + /// public void Dispose() { @@ -848,7 +892,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// Writes the chunk end to the stream. /// /// The containing image data. - private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null); + private void WriteEndChunk(Stream stream) + { + this.WriteChunk(stream, PngChunkType.End, null); + } /// /// Writes a chunk to the stream. @@ -856,7 +903,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// The to write to. /// The type of chunk to write. /// The containing data. - private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); + private void WriteChunk(Stream stream, PngChunkType type, byte[] data) + { + this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); + } /// /// Writes a chunk of a specified length to the stream at the given offset. diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 0014defbb..765222795 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -47,22 +47,22 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets or sets the Rgb 24 transparent color. This represents any color in an 8 bit Rgb24 encoded png that should be transparent /// - public Rgb24 Rgb24Trans { get; set; } + public Rgb24? Rgb24Trans { get; set; } /// /// Gets or sets the Rgb 48 transparent color. This represents any color in a 16 bit Rgb24 encoded png that should be transparent /// - public Rgb48 Rgb48Trans { get; set; } + public Rgb48? Rgb48Trans { get; set; } /// /// Gets or sets the 8 bit grayscale transparent color. This represents any color in an 8 bit grayscale encoded png that should be transparent /// - public byte LuminanceTrans { get; set; } + public byte? LuminanceTrans { get; set; } /// /// Gets or sets the 16 bit grayscale transparent color. This represents any color in a 16 bit grayscale encoded png that should be transparent /// - public ushort Luminance16Trans { get; set; } + public ushort? Luminance16Trans { get; set; } /// /// Gets or sets a value indicating whether the image has transparency chunk and markers were decoded From fae06caff766697f82807aed30b4c5fe623b2b39 Mon Sep 17 00:00:00 2001 From: Devedse Date: Wed, 31 Oct 2018 01:27:11 +0100 Subject: [PATCH 03/66] Fixed bug, code now actually fixed my unit test --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c76dc308b..edf6cecd7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -327,9 +327,9 @@ namespace SixLabors.ImageSharp.Formats.Png else if (pngMetaData.Rgb24Trans != null) { var alphaArray = new byte[6]; - alphaArray[0] = pngMetaData.Rgb24Trans.Value.R; - alphaArray[2] = pngMetaData.Rgb24Trans.Value.G; - alphaArray[4] = pngMetaData.Rgb24Trans.Value.B; + alphaArray[1] = pngMetaData.Rgb24Trans.Value.R; + alphaArray[3] = pngMetaData.Rgb24Trans.Value.G; + alphaArray[5] = pngMetaData.Rgb24Trans.Value.B; this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } } @@ -344,7 +344,9 @@ namespace SixLabors.ImageSharp.Formats.Png else if (pngMetaData.LuminanceTrans != null) { var alphaArray = new byte[2]; - alphaArray[0] = pngMetaData.LuminanceTrans.Value; + alphaArray[1] = pngMetaData.LuminanceTrans.Value; + + this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } } } From 02a8d0ddc185979175a4af170bc43e88bc493807 Mon Sep 17 00:00:00 2001 From: Devedse <2350015+devedse@users.noreply.github.com> Date: Wed, 31 Oct 2018 02:04:59 +0100 Subject: [PATCH 04/66] Update PngEncoderCore.cs Lowercase b --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index edf6cecd7..1179a6db3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -318,9 +318,9 @@ namespace SixLabors.ImageSharp.Formats.Png { var r = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); var g = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); - var B = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.B); + var b = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.B); - var alphaArray = r.Concat(g).Concat(B).ToArray(); + var alphaArray = r.Concat(g).Concat(b).ToArray(); this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } @@ -1004,4 +1004,4 @@ namespace SixLabors.ImageSharp.Formats.Png return scanlineLength / mod; } } -} \ No newline at end of file +} From a7aef83dcceb7d291b7bed81277e675be2d65ca7 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 2 Nov 2018 17:36:38 +0100 Subject: [PATCH 05/66] Removed unused using --- src/ImageSharp/MetaData/ImageMetaData.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index ec9037479..73549d98a 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Icc; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.MetaData { From 6364b502da9aed140aa8430ccdddb148acf6efd3 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 2 Nov 2018 17:49:35 +0100 Subject: [PATCH 06/66] Resolved findings in PR --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 24 +++++++++---------- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 24 +++++++++---------- src/ImageSharp/Formats/Png/PngMetaData.cs | 8 +++---- .../Formats/Png/PngScanlineProcessor.cs | 16 ++++++------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 87c22a2ad..c446184d8 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -656,8 +656,8 @@ namespace SixLabors.ImageSharp.Formats.Png scanlineSpan, rowSpan, pngMetaData.HasTrans, - pngMetaData.Luminance16Trans.GetValueOrDefault(), - pngMetaData.LuminanceTrans.GetValueOrDefault()); + pngMetaData.TransparentGray16.GetValueOrDefault(), + pngMetaData.TransparentGray8.GetValueOrDefault()); break; @@ -690,8 +690,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerPixel, this.bytesPerSample, pngMetaData.HasTrans, - pngMetaData.Rgb48Trans.GetValueOrDefault(), - pngMetaData.Rgb24Trans.GetValueOrDefault()); + pngMetaData.TransparentRgb48.GetValueOrDefault(), + pngMetaData.TransparentRgb24.GetValueOrDefault()); break; @@ -740,8 +740,8 @@ namespace SixLabors.ImageSharp.Formats.Png pixelOffset, increment, pngMetaData.HasTrans, - pngMetaData.Luminance16Trans.GetValueOrDefault(), - pngMetaData.LuminanceTrans.GetValueOrDefault()); + pngMetaData.TransparentGray16.GetValueOrDefault(), + pngMetaData.TransparentGray8.GetValueOrDefault()); break; @@ -779,8 +779,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerPixel, this.bytesPerSample, pngMetaData.HasTrans, - pngMetaData.Rgb48Trans.GetValueOrDefault(), - pngMetaData.Rgb24Trans.GetValueOrDefault()); + pngMetaData.TransparentRgb48.GetValueOrDefault(), + pngMetaData.TransparentRgb24.GetValueOrDefault()); break; @@ -817,7 +817,7 @@ namespace SixLabors.ImageSharp.Formats.Png ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); - pngMetaData.Rgb48Trans = new Rgb48(rc, gc, bc); + pngMetaData.TransparentRgb48 = new Rgb48(rc, gc, bc); pngMetaData.HasTrans = true; return; } @@ -825,7 +825,7 @@ namespace SixLabors.ImageSharp.Formats.Png byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); - pngMetaData.Rgb24Trans = new Rgb24(r, g, b); + pngMetaData.TransparentRgb24 = new Rgb24(r, g, b); pngMetaData.HasTrans = true; } } @@ -835,11 +835,11 @@ namespace SixLabors.ImageSharp.Formats.Png { if (this.header.BitDepth == 16) { - pngMetaData.Luminance16Trans = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)); + pngMetaData.TransparentGray16 = new Gray16(BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2))); } else { - pngMetaData.LuminanceTrans = ReadByteLittleEndian(alpha, 0); + pngMetaData.TransparentGray8 = new Gray8(ReadByteLittleEndian(alpha, 0)); } pngMetaData.HasTrans = true; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 1179a6db3..e953bc1f0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -314,37 +314,37 @@ namespace SixLabors.ImageSharp.Formats.Png { if (pngMetaData.ColorType == PngColorType.Rgb) { - if (pngMetaData.Rgb48Trans != null) + if (pngMetaData.TransparentRgb48 != null) { - var r = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); - var g = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.R); - var b = BitConverter.GetBytes(pngMetaData.Rgb48Trans.Value.B); + var r = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.R); + var g = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.R); + var b = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.B); var alphaArray = r.Concat(g).Concat(b).ToArray(); this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } - else if (pngMetaData.Rgb24Trans != null) + else if (pngMetaData.TransparentRgb24 != null) { var alphaArray = new byte[6]; - alphaArray[1] = pngMetaData.Rgb24Trans.Value.R; - alphaArray[3] = pngMetaData.Rgb24Trans.Value.G; - alphaArray[5] = pngMetaData.Rgb24Trans.Value.B; + alphaArray[1] = pngMetaData.TransparentRgb24.Value.R; + alphaArray[3] = pngMetaData.TransparentRgb24.Value.G; + alphaArray[5] = pngMetaData.TransparentRgb24.Value.B; this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } } else if (pngMetaData.ColorType == PngColorType.Grayscale) { - if (pngMetaData.Luminance16Trans != null) + if (pngMetaData.TransparentGray16 != null) { - var alphaArray = BitConverter.GetBytes(pngMetaData.Luminance16Trans.Value); + var alphaArray = BitConverter.GetBytes(pngMetaData.TransparentGray16.Value.PackedValue); this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } - else if (pngMetaData.LuminanceTrans != null) + else if (pngMetaData.TransparentGray8 != null) { var alphaArray = new byte[2]; - alphaArray[1] = pngMetaData.LuminanceTrans.Value; + alphaArray[1] = pngMetaData.TransparentGray8.Value.PackedValue; this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); } diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 765222795..6a293f770 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -47,22 +47,22 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets or sets the Rgb 24 transparent color. This represents any color in an 8 bit Rgb24 encoded png that should be transparent /// - public Rgb24? Rgb24Trans { get; set; } + public Rgb24? TransparentRgb24 { get; set; } /// /// Gets or sets the Rgb 48 transparent color. This represents any color in a 16 bit Rgb24 encoded png that should be transparent /// - public Rgb48? Rgb48Trans { get; set; } + public Rgb48? TransparentRgb48 { get; set; } /// /// Gets or sets the 8 bit grayscale transparent color. This represents any color in an 8 bit grayscale encoded png that should be transparent /// - public byte? LuminanceTrans { get; set; } + public Gray8? TransparentGray8 { get; set; } /// /// Gets or sets the 16 bit grayscale transparent color. This represents any color in a 16 bit grayscale encoded png that should be transparent /// - public ushort? Luminance16Trans { get; set; } + public Gray16? TransparentGray16 { get; set; } /// /// Gets or sets a value indicating whether the image has transparency chunk and markers were decoded diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 3fe590ee2..528c012c5 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Formats.Png ReadOnlySpan scanlineSpan, Span rowSpan, bool hasTrans, - ushort luminance16Trans, - byte luminanceTrans) + Gray16 luminance16Trans, + Gray8 luminanceTrans) where TPixel : struct, IPixel { TPixel pixel = default; @@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Png rgba64.R = luminance; rgba64.G = luminance; rgba64.B = luminance; - rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue; + rgba64.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Png } else { - byte scaledLuminanceTrans = (byte)(luminanceTrans * scaleFactor); + byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); Rgba32 rgba32 = default; for (int x = 0; x < header.Width; x++) { @@ -93,8 +93,8 @@ namespace SixLabors.ImageSharp.Formats.Png int pixelOffset, int increment, bool hasTrans, - ushort luminance16Trans, - byte luminanceTrans) + Gray16 luminance16Trans, + Gray8 luminanceTrans) where TPixel : struct, IPixel { TPixel pixel = default; @@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Formats.Png rgba64.R = luminance; rgba64.G = luminance; rgba64.B = luminance; - rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue; + rgba64.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Png } else { - byte scaledLuminanceTrans = (byte)(luminanceTrans * scaleFactor); + byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); Rgba32 rgba32 = default; for (int x = pixelOffset; x < header.Width; x += increment) { From 300ee6a555455624ab9d025be6be9a6b14040262 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Nov 2018 16:11:45 +0100 Subject: [PATCH 07/66] move ResizeProfilingBenchmarks, use the ***ProfilingBenchmarks naming convention everywhere --- tests/ImageSharp.Sandbox46/Program.cs | 4 ++-- .../{JpegBenchmarks.cs => JpegProfilingBenchmarks.cs} | 4 ++-- ...hmarks.cs => LoadResizeSaveProfilingBenchmarks.cs} | 4 ++-- .../ResizeProfilingBenchmarks.cs | 11 ++--------- 4 files changed, 8 insertions(+), 15 deletions(-) rename tests/ImageSharp.Tests/ProfilingBenchmarks/{JpegBenchmarks.cs => JpegProfilingBenchmarks.cs} (96%) rename tests/ImageSharp.Tests/ProfilingBenchmarks/{LoadResizeSaveBenchmarks.cs => LoadResizeSaveProfilingBenchmarks.cs} (89%) rename tests/ImageSharp.Tests/{Processing/Processors/Transforms => ProfilingBenchmarks}/ResizeProfilingBenchmarks.cs (82%) diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index c0bb25a1b..02d4f80c5 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -63,8 +63,8 @@ namespace SixLabors.ImageSharp.Sandbox46 private static void RunDecodeJpegProfilingTests() { Console.WriteLine("RunDecodeJpegProfilingTests..."); - var benchmarks = new JpegBenchmarks(new ConsoleOutput()); - foreach (object[] data in JpegBenchmarks.DecodeJpegData) + var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput()); + foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData) { string fileName = (string)data[0]; benchmarks.DecodeJpeg(fileName); diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs similarity index 96% rename from tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs index 5bc1693bc..609aa43b7 100644 --- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs @@ -15,9 +15,9 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { - public class JpegBenchmarks : MeasureFixture + public class JpegProfilingBenchmarks : MeasureFixture { - public JpegBenchmarks(ITestOutputHelper output) + public JpegProfilingBenchmarks(ITestOutputHelper output) : base(output) { } diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs similarity index 89% rename from tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs index 306072276..95fe4e48f 100644 --- a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs @@ -10,9 +10,9 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { - public class LoadResizeSaveBenchmarks : MeasureFixture + public class LoadResizeSaveProfilingBenchmarks : MeasureFixture { - public LoadResizeSaveBenchmarks(ITestOutputHelper output) + public LoadResizeSaveProfilingBenchmarks(ITestOutputHelper output) : base(output) { } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs similarity index 82% rename from tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs index e24458d38..8b9355938 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs @@ -7,17 +7,10 @@ using SixLabors.ImageSharp.Processing; using Xunit; using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms +namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { public class ResizeProfilingBenchmarks : MeasureFixture { - public const string SkipText = -#if false - null; -#else - "Benchmark, enable manually!"; -#endif - private readonly Configuration configuration = Configuration.CreateDefaultInstance(); public ResizeProfilingBenchmarks(ITestOutputHelper output) @@ -28,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public int ExecutionCount { get; set; } = 50; - [Theory(Skip = SkipText)] + [Theory(Skip = ProfilingSetup.SkipProfilingTests)] [InlineData(100, 100)] [InlineData(2000, 2000)] public void ResizeBicubic(int width, int height) From f45ed5dee0611426555677328e8e747179396c69 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Nov 2018 16:59:15 +0100 Subject: [PATCH 08/66] simplify ResizeKernel.Convolve(...) --- .../Processing/Processors/Transforms/ResizeKernel.cs | 4 ++-- .../Processors/Transforms/ResizeProcessor.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs index cc3c20453..be4b7a741 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs @@ -73,11 +73,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The source row position. /// The weighted sum [MethodImpl(InliningOptions.ShortMethod)] - public Vector4 Convolve(Span rowSpan, int sourceX) + public Vector4 Convolve(Span rowSpan) { ref float horizontalValues = ref this.GetStartReference(); int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left + sourceX); + ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left); // Destination color components Vector4 result = Vector4.Zero; diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index 4d4ed06ce..d0d225d9b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -254,8 +254,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = source.GetPixelRowSpan(y); - Span tempRowSpan = tempRowBuffer.Span; + Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); + Span tempRowSpan = tempRowBuffer.Span.Slice(sourceX); PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan); Vector4Utils.Premultiply(tempRowSpan); @@ -271,7 +271,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX]; Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = - window.Convolve(tempRowSpan, sourceX); + window.Convolve(tempRowSpan); } } }); @@ -295,10 +295,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { - Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x); + Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); // Destination color components - Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn, sourceY); + Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn); } Vector4Utils.UnPremultiply(tempRowSpan); From 32e0497dca2f1921d5903ef82f69c6d564728237 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Nov 2018 19:59:06 +0100 Subject: [PATCH 09/66] minor refactor on ResizeKernel --- .../Processors/Transforms/KernelMap.cs | 3 ++- .../Processors/Transforms/ResizeKernel.cs | 22 ++++--------------- .../Processors/Transforms/KernelMapTests.cs | 14 +++++++----- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index 277be53ff..f7a3a6f6d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.Memory; @@ -89,7 +90,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ResizeKernel ws = result.CreateKernel(i, left, right); result.Kernels[i] = ws; - ref float weightsBaseRef = ref ws.GetStartReference(); + ref float weightsBaseRef = ref MemoryMarshal.GetReference(ws.GetValues()); for (int j = left; j <= right; j++) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs index be4b7a741..707f1467b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs @@ -2,13 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; -using SixLabors.Memory; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { @@ -18,12 +16,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal struct ResizeKernel { /// - /// The local left index position + /// The left index for the destination row /// public int Left; /// - /// The length of the weights window + /// The length of the kernel /// public int Length; @@ -48,34 +46,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.Length = length; } - /// - /// Gets a reference to the first item of the window. - /// - /// The reference to the first item of the window - [MethodImpl(InliningOptions.ShortMethod)] - public ref float GetStartReference() - { - Span span = this.buffer.Span; - return ref span[0]; - } - /// /// Gets the span representing the portion of the that this window covers /// /// The [MethodImpl(InliningOptions.ShortMethod)] - public Span GetSpan() => this.buffer.Span; + public Span GetValues() => this.buffer.Span; /// /// 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(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) { - ref float horizontalValues = ref this.GetStartReference(); + ref float horizontalValues = ref MemoryMarshal.GetReference(this.GetValues()); int left = this.Left; ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 1b4b3cf6a..a7d93ad1d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -21,10 +21,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms this.Output = output; } - [Theory(Skip = "TODO: Add asserionts")] + [Theory] [InlineData(500, 200, nameof(KnownResamplers.Bicubic))] [InlineData(50, 40, nameof(KnownResamplers.Bicubic))] [InlineData(40, 30, nameof(KnownResamplers.Bicubic))] + [InlineData(15, 10, nameof(KnownResamplers.Bicubic))] [InlineData(500, 200, nameof(KnownResamplers.Lanczos8))] [InlineData(100, 80, nameof(KnownResamplers.Lanczos8))] [InlineData(100, 10, nameof(KnownResamplers.Lanczos8))] @@ -37,14 +38,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var bld = new StringBuilder(); - foreach (ResizeKernel window in kernelMap.Kernels) + foreach (ResizeKernel kernel in kernelMap.Kernels) { - Span span = window.GetSpan(); - for (int i = 0; i < window.Length; i++) + bld.Append($"({kernel.Left:D3}) || "); + Span span = kernel.GetValues(); + for (int i = 0; i < kernel.Length; i++) { float value = span[i]; - bld.Append($"{value,7:F4}"); - bld.Append("| "); + bld.Append($"{value,7:F5}"); + bld.Append(" | "); } bld.AppendLine(); From f425190dc7f7bb0c686d251cb31f04d3b9785b84 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Nov 2018 21:59:18 +0100 Subject: [PATCH 10/66] Cover KernelMap with tests --- .../Processors/Transforms/KernelMap.cs | 18 ++-- .../Processors/Transforms/ResizeProcessor.cs | 8 +- .../KernelMapTests.ReferenceKernelMap.cs | 99 +++++++++++++++++++ .../Processors/Transforms/KernelMapTests.cs | 42 +++++--- 4 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index f7a3a6f6d..b0fd0e2cd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -17,6 +17,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { private readonly Buffer2D data; + private readonly ResizeKernel[] kernels; + /// /// Initializes a new instance of the class. /// @@ -25,15 +27,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The radius of the kernel public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) { + this.DestinationSize = destinationSize; int width = (int)Math.Ceiling(kernelRadius * 2); this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); - this.Kernels = new ResizeKernel[destinationSize]; + this.kernels = new ResizeKernel[destinationSize]; } - /// - /// Gets the calculated values. - /// - public ResizeKernel[] Kernels { get; } + public int DestinationSize { get; } /// /// Disposes instance releasing it's backing buffer. @@ -43,6 +43,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.data.Dispose(); } + /// + /// Returns a for an index value between 0 and destinationSize - 1. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx]; + /// /// Computes the weights to apply at each pixel when resizing. /// @@ -88,7 +94,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float sum = 0; ResizeKernel ws = result.CreateKernel(i, left, right); - result.Kernels[i] = ws; + result.kernels[i] = ws; ref float weightsBaseRef = ref MemoryMarshal.GetReference(ws.GetValues()); diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index d0d225d9b..7c9d39fc5 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -269,9 +269,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = minX; x < maxX; x++) { - ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX]; + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX); Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = - window.Convolve(tempRowSpan); + kernel.Convolve(tempRowSpan); } } }); @@ -289,7 +289,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int y = rows.Min; y < rows.Max; y++) { // Ensure offsets are normalized for cropping and padding. - ResizeKernel window = this.verticalKernelMap.Kernels[y - startY]; + ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY); ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan); @@ -298,7 +298,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); // Destination color components - Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn); + Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); } Vector4Utils.UnPremultiply(tempRowSpan); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs new file mode 100644 index 000000000..932ea5494 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; + +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms +{ + public partial class KernelMapTests + { + /// + /// Simplified reference implementation for functionality. + /// + public class ReferenceKernelMap + { + private readonly ReferenceKernel[] kernels; + + public ReferenceKernelMap(ReferenceKernel[] kernels) + { + this.kernels = kernels; + } + + public int DestinationSize => this.kernels.Length; + + public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex]; + + public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize) + { + float ratio = (float)sourceSize / destinationSize; + float scale = ratio; + + if (scale < 1F) + { + scale = 1F; + } + + float radius = MathF.Ceiling(scale * sampler.Radius); + + var result = new List(); + + for (int i = 0; i < destinationSize; i++) + { + float center = ((i + .5F) * ratio) - .5F; + + // Keep inside bounds. + int left = (int)MathF.Ceiling(center - radius); + if (left < 0) + { + left = 0; + } + + int right = (int)MathF.Floor(center + radius); + if (right > sourceSize - 1) + { + right = sourceSize - 1; + } + + float sum = 0; + + float[] values = new float[right - left + 1]; + + for (int j = left; j <= right; j++) + { + float weight = sampler.GetValue((j - center) / scale); + sum += weight; + + values[j - left] = weight; + } + + result.Add(new ReferenceKernel(left, values)); + + if (sum > 0) + { + for (int w = 0; w < values.Length; w++) + { + values[w] /= sum; + } + } + } + + return new ReferenceKernelMap(result.ToArray()); + } + } + + public struct ReferenceKernel + { + public ReferenceKernel(int left, float[] values) + { + this.Left = left; + this.Values = values; + } + + public int Left { get; } + + public float[] Values { get; } + + public int Length => this.Values.Length; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index a7d93ad1d..9f4d53b96 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -12,7 +12,7 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - public class KernelMapTests + public partial class KernelMapTests { private ITestOutputHelper Output { get; } @@ -30,34 +30,52 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [InlineData(100, 80, nameof(KnownResamplers.Lanczos8))] [InlineData(100, 10, nameof(KnownResamplers.Lanczos8))] [InlineData(10, 100, nameof(KnownResamplers.Lanczos8))] - public void PrintKernelMap(int srcSize, int destSize, string resamplerName) + public void KernelMapContentIsCorrect(int srcSize, int destSize, string resamplerName) { var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); + var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); + +#if DEBUG + string text = PrintKernelMap(kernelMap); + this.Output.WriteLine(text); +#endif + + for (int i = 0; i < kernelMap.DestinationSize; i++) + { + ResizeKernel kernel = kernelMap.GetKernel(i); + + ReferenceKernel referenceKernel = referenceMap.GetKernel(i); + + Assert.Equal(referenceKernel.Length, kernel.Length); + Assert.Equal(referenceKernel.Left, kernel.Left); + Assert.True(kernel.GetValues().SequenceEqual(referenceKernel.Values)); + } + } + + private static string PrintKernelMap(KernelMap kernelMap) + { var bld = new StringBuilder(); - foreach (ResizeKernel kernel in kernelMap.Kernels) + for (int i = 0; i < kernelMap.DestinationSize; i++) { + ResizeKernel kernel = kernelMap.GetKernel(i); bld.Append($"({kernel.Left:D3}) || "); Span span = kernel.GetValues(); - for (int i = 0; i < kernel.Length; i++) + + for (int j = 0; j < kernel.Length; j++) { - float value = span[i]; - bld.Append($"{value,7:F5}"); + float value = span[j]; + bld.Append($"{value,8:F5}"); bld.Append(" | "); } bld.AppendLine(); } - string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap)); - string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD"; - - File.WriteAllText(fileName, bld.ToString()); - - this.Output.WriteLine(bld.ToString()); + return bld.ToString(); } } } \ No newline at end of file From 41972e90c8e1f69b14d920adc368a8fd8c6f3db2 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 4 Nov 2018 22:17:46 +0100 Subject: [PATCH 11/66] refactor ResizeKernel creation --- .../Processors/Transforms/KernelMap.cs | 13 ++++--- .../Processors/Transforms/ResizeKernel.cs | 34 ++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index b0fd0e2cd..278fd93d8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The to use for allocations. /// The size of the destination window /// The radius of the kernel - public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) + private KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) { this.DestinationSize = destinationSize; int width = (int)Math.Ceiling(kernelRadius * 2); @@ -125,13 +125,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// 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 - private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx) + private ResizeKernel CreateKernel(int destIdx, int left, int rightIdx) { - return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1); + int flatStartIndex = destIdx * this.data.Width; + int length = rightIdx - left + 1; + Memory bufferSlice = this.data.Memory.Slice(flatStartIndex, length); + return new ResizeKernel(left, bufferSlice); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs index 707f1467b..1ce9c9c91 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs @@ -16,42 +16,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal struct ResizeKernel { /// - /// The left index for the destination row + /// Initializes a new instance of the struct. /// - public int Left; + [MethodImpl(InliningOptions.ShortMethod)] + internal ResizeKernel(int left, Memory bufferSlice) + { + this.Left = left; + this.BufferSlice = bufferSlice; + } /// - /// The length of the kernel + /// Gets the left index for the destination row /// - public int Length; + public int Left { get; } /// - /// The buffer containing the weights values. + /// Gets the slice of the buffer containing the weights values. /// - private readonly Memory buffer; + public Memory BufferSlice { get; } /// - /// Initializes a new instance of the struct. + /// Gets the the length of the kernel /// - /// The destination index in the buffer - /// The local left index - /// The span - /// The length of the window - [MethodImpl(InliningOptions.ShortMethod)] - internal ResizeKernel(int index, int left, Buffer2D buffer, int length) - { - int flatStartIndex = index * buffer.Width; - this.Left = left; - this.buffer = buffer.MemorySource.Memory.Slice(flatStartIndex, length); - this.Length = length; - } + public int Length => this.BufferSlice.Length; /// /// Gets the span representing the portion of the that this window covers /// /// The [MethodImpl(InliningOptions.ShortMethod)] - public Span GetValues() => this.buffer.Span; + public Span GetValues() => this.BufferSlice.Span; /// /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. From e059620f19f9391e40806d6961bc00813a56494d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 5 Nov 2018 01:34:03 +0100 Subject: [PATCH 12/66] KernelMap refactor WIP --- .../ImageSharp.Drawing.csproj | 3 +- src/ImageSharp/ImageSharp.csproj | 3 +- .../Processors/Transforms/KernelMap.cs | 34 ++++++++++----- .../ImageSharp.Benchmarks.csproj | 3 +- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 3 +- .../Processors/Transforms/KernelMapTests.cs | 42 ++++++++++++++----- 6 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 1cb3f444f..6341e1771 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -5,7 +5,8 @@ $(packageversion) 0.0.1 SixLabors and contributors - netstandard1.3;netstandard2.0 + + netcoreapp2.1 7.3 true true diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 29d29d50d..e2f55e3c6 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -5,7 +5,8 @@ $(packageversion) 0.0.1 Six Labors and contributors - netstandard1.3;netstandard2.0;netcoreapp2.1;net472 + + netcoreapp2.1 true true SixLabors.ImageSharp diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index 278fd93d8..3f984fef0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -19,16 +19,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ResizeKernel[] kernels; - /// - /// Initializes a new instance of the class. - /// - /// The to use for allocations. - /// The size of the destination window - /// The radius of the kernel - private KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) + private int period; + + private int radius; + + private int periodicRegionMin; + + private int periodicRegionMax; + + private KernelMap(MemoryAllocator memoryAllocator, int destinationSize, int radius, int period) { this.DestinationSize = destinationSize; - int width = (int)Math.Ceiling(kernelRadius * 2); + this.period = period; + this.radius = radius; + this.periodicRegionMin = period + radius; + this.periodicRegionMax = destinationSize - radius; + + int width = radius * 2; this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); this.kernels = new ResizeKernel[destinationSize]; } @@ -71,8 +78,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms scale = 1F; } - float radius = MathF.Ceiling(scale * sampler.Radius); - var result = new KernelMap(memoryAllocator, destinationSize, radius); + int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; + + int radius = (int)MathF.Ceiling(scale * sampler.Radius); + var result = new KernelMap(memoryAllocator, destinationSize, radius, period); for (int i = 0; i < destinationSize; i++) { @@ -122,6 +131,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return result; } + private int ReduceIndex(int destIndex) + { + return destIndex; + } + /// /// Slices a weights value at the given positions. /// diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index a705c9bac..04a4541b2 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -1,6 +1,7 @@  - netcoreapp2.1;net461 + + netcoreapp2.1 Exe True SixLabors.ImageSharp.Benchmarks diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 86c1a7a25..75ac7450c 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -1,6 +1,7 @@  - net462;net472;netcoreapp2.1 + + netcoreapp2.1 True latest full diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 9f4d53b96..7abc1c312 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -20,17 +20,39 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { this.Output = output; } - + + /// + /// resamplerName, srcSize, destSize + /// + public static readonly TheoryData KernelMapData = new TheoryData + { + { nameof(KnownResamplers.Bicubic), 15, 10 }, + { nameof(KnownResamplers.Bicubic), 10, 15 }, + { nameof(KnownResamplers.Bicubic), 20, 20 }, + { nameof(KnownResamplers.Bicubic), 50, 40 }, + { nameof(KnownResamplers.Bicubic), 40, 50 }, + { nameof(KnownResamplers.Bicubic), 500, 200 }, + { nameof(KnownResamplers.Bicubic), 200, 500 }, + + { nameof(KnownResamplers.Lanczos3), 16, 12 }, + { nameof(KnownResamplers.Lanczos3), 12, 16 }, + { nameof(KnownResamplers.Lanczos3), 12, 9 }, + { nameof(KnownResamplers.Lanczos3), 9, 12 }, + { nameof(KnownResamplers.Lanczos3), 6, 8 }, + { nameof(KnownResamplers.Lanczos3), 8, 6 }, + + // TODO: What's wrong here: + // { nameof(KnownResamplers.Lanczos3), 20, 12 }, + + {nameof(KnownResamplers.Lanczos8), 500, 200 }, + {nameof(KnownResamplers.Lanczos8), 100, 10 }, + {nameof(KnownResamplers.Lanczos8), 100, 80 }, + {nameof(KnownResamplers.Lanczos8), 10, 100 }, + }; + [Theory] - [InlineData(500, 200, nameof(KnownResamplers.Bicubic))] - [InlineData(50, 40, nameof(KnownResamplers.Bicubic))] - [InlineData(40, 30, nameof(KnownResamplers.Bicubic))] - [InlineData(15, 10, nameof(KnownResamplers.Bicubic))] - [InlineData(500, 200, nameof(KnownResamplers.Lanczos8))] - [InlineData(100, 80, nameof(KnownResamplers.Lanczos8))] - [InlineData(100, 10, nameof(KnownResamplers.Lanczos8))] - [InlineData(10, 100, nameof(KnownResamplers.Lanczos8))] - public void KernelMapContentIsCorrect(int srcSize, int destSize, string resamplerName) + [MemberData(nameof(KernelMapData))] + public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); From e79f26bc7ec9cfca5bedb2309d7b7486c9da79ea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 5 Nov 2018 10:55:22 +0000 Subject: [PATCH 13/66] Add base becnhmark --- .../InterpolatedTransformProcessorBase.cs | 5 +-- .../ImageSharp.Benchmarks/Samplers/Rotate.cs | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Samplers/Rotate.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs index c1abb4a5e..4737a4102 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int i = 0; i < length; i++) { ref float wRef = ref Unsafe.Add(ref weightsRef, i); - wRef = wRef / sum; + wRef /= sum; } } } @@ -90,8 +90,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) { - float weight = sampler.GetValue(i - point); - Unsafe.Add(ref weightsRef, x) = weight; + Unsafe.Add(ref weightsRef, x) = sampler.GetValue(i - point); } } diff --git a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs new file mode 100644 index 000000000..c1456f9d7 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs @@ -0,0 +1,39 @@ +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Samplers +{ + [Config(typeof(Config.ShortClr))] + public class Rotate + { + [Benchmark] + public Size DoRotate() + { + using (var image = new Image(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) + { + image.Mutate(x => x.Rotate(37.5F)); + + return image.Size(); + } + } + } +} + +// Nov 4 2018 +//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 +//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores +//.NET Core SDK = 2.1.403 + +// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT +// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 +// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT + +//LaunchCount=1 TargetCount=3 WarmupCount=3 + +// #### BEFORE ####: +// Method | Runtime | Mean | Error | StdDev | Allocated | +//--------- |-------- |---------:|----------:|----------:|----------:| +// DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB | +// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | \ No newline at end of file From dfc926e5692cadfc5c5b0d3b59cdcea9225ba7fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 6 Nov 2018 17:27:21 +0000 Subject: [PATCH 14/66] Better Rotate --- .../Transforms/AffineTransformProcessor.cs | 204 ++++----------- .../Transforms/AffineTransformProcessorOld.cs | 239 ++++++++++++++++++ .../CenteredAffineTransformProcessor.cs | 2 +- .../Processors/Transforms/RotateProcessor.cs | 16 +- .../Transforms/TransformKernelMap.cs | 188 ++++++++++++++ .../Processors/Transforms/TransformUtils.cs | 121 +++++++++ .../Processing/TransformExtensions.cs | 6 +- 7 files changed, 611 insertions(+), 165 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index e12b91eab..edddab181 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -5,13 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -20,29 +16,42 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides the base methods to perform affine transforms on an image. /// /// The pixel format. - internal class AffineTransformProcessor : InterpolatedTransformProcessorBase + internal class AffineTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { + private readonly Rectangle transformedRectangle; + /// /// Initializes a new instance of the class. /// /// The transform matrix /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) + /// The source image size + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) { + Guard.NotNull(sampler, nameof(sampler)); + this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; + this.transformedRectangle = TransformUtils.GetTransformedRectangle( + new Rectangle(Point.Empty, sourceSize), + matrix); + + // We want to resize the canvas here taking into account any translations. + this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); } /// - /// Gets the matrix used to supply the affine transform + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + + /// + /// Gets the matrix used to supply the affine transform. /// public Matrix3x2 TransformMatrix { get; } /// - /// Gets the target dimensions to constrain the transformed image to + /// Gets the target dimensions to constrain the transformed image to. /// public Size TargetDimensions { get; } @@ -68,13 +77,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms 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); + var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. - Matrix3x2.Invert(matrix, out matrix); + Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 matrix); if (this.Sampler is NearestNeighborResampler) { @@ -82,158 +88,52 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms targetBounds, configuration, rows => + { + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span destRow = destination.GetPixelRowSpan(y); + Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) + for (int x = 0; x < width; x++) + { + var point = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(point.X, point.Y)) { - var point = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(point.X, point.Y)) - { - destRow[x] = source[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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler)) { - ParallelHelper.IterateRows( + ParallelHelper.IterateRowsWithTempBuffer( targetBounds, configuration, - rows => + (rows, vectorBuffer) => + { + Span vectorSpan = vectorBuffer.Span; + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); + Span targetRowSpan = destination.GetPixelRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, targetRowSpan, vectorSpan); + ref float ySpanRef = ref kernel.GetYStartReference(y); + ref float xSpanRef = ref kernel.GetXStartReference(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. - // Precalculating 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } + 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); + kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); } - }); + + PixelOperations.Instance.FromVector4(configuration, vectorSpan, targetRowSpan); + } + }); } } - - /// - /// 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 Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => this.TransformMatrix; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs new file mode 100644 index 000000000..5891afd9a --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs @@ -0,0 +1,239 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Provides the base methods to perform affine transforms on an image. + /// + /// The pixel format. + internal class AffineTransformProcessorOld : InterpolatedTransformProcessorBase + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix + /// The sampler to perform the transform operation. + /// The target dimensions to constrain the transformed image to. + public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) + : base(sampler) + { + this.TransformMatrix = matrix; + this.TargetDimensions = targetDimensions; + } + + /// + /// Gets the matrix used to supply the affine transform + /// + public Matrix3x2 TransformMatrix { get; } + + /// + /// Gets the target dimensions to constrain the transformed image to + /// + public Size TargetDimensions { get; } + + /// + protected override Image CreateDestination(Image source, Rectangle 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(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); + + // Use the overload to prevent an extra frame being added + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); + } + + /// + protected override void OnFrameApply( + ImageFrame source, + ImageFrame destination, + Rectangle sourceRectangle, + Configuration configuration) + { + 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); + + // Convert from screen to world space. + Matrix3x2.Invert(matrix, out matrix); + + if (this.Sampler is NearestNeighborResampler) + { + ParallelHelper.IterateRows( + targetBounds, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var point = Point.Transform(new Point(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); + + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + + using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) + using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + { + ParallelHelper.IterateRows( + targetBounds, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(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. + // Precalculating 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, + ref ySpanRef, + yLength); + + CalculateWeightsDown( + left, + right, + minX, + maxX, + point.X, + sampler, + xScale, + ref xSpanRef, + xLength); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); + } + + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) + { + float yWeight = Unsafe.Add(ref ySpanRef, yy); + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = Unsafe.Add(ref xSpanRef, xx); + + // Values are first premultiplied to prevent darkening of edge pixels + var current = source[i, j].ToVector4(); + Vector4Utils.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + + // Reverse the premultiplication + Vector4Utils.UnPremultiply(ref sum); + dest.FromVector4(sum); + } + } + }); + } + } + + /// + /// 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 Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) + => this.TransformMatrix; + } +} \ 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 adaee1766..82614dc8c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// A base class that provides methods to allow the automatic centering of affine transforms /// /// The pixel format. - internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessor + internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessorOld where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 2ad626755..cbf82cc9b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -2,12 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -16,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides methods that allow the rotating of images. /// /// The pixel format. - internal class RotateProcessor : CenteredAffineTransformProcessor + internal class RotateProcessor : AffineTransformProcessor where TPixel : struct, IPixel { /// @@ -36,10 +34,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the rotating operation. /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) - : base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler, sourceSize) - { - this.Degrees = degrees; - } + : base( + TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize), + sampler, + sourceSize) + => this.Degrees = degrees; /// /// Gets the angle of rotation in degrees. @@ -84,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The private static float WrapDegrees(float degrees) { - degrees = degrees % 360; + degrees %= 360; while (degrees < 0) { @@ -223,7 +222,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int newX = height - y - 1; for (int x = 0; x < width; x++) { - // TODO: Optimize this: if (destinationBounds.Contains(newX, x)) { destination[newX, x] = sourceRow[x]; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs new file mode 100644 index 000000000..5acc7213c --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +// TODO: It would be great if we could somehow optimize this to calculate the weights once. +// currently we cannot do that as we are calulating the weight of the transformed point dimension +// not the point in the original image. +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Contains the methods required to calculate kernel sampling weights on-the-fly. + /// + internal class TransformKernelMap : IDisposable + { + private readonly Buffer2D yBuffer; + private readonly Buffer2D xBuffer; + private readonly float yScale; + private readonly float xScale; + private readonly int yLength; + private readonly int xLength; + private readonly Vector2 extents; + private Vector4 maxSourceExtents; + private readonly IResampler sampler; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The source size. + /// The destination size. + /// The sampler. + public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) + { + this.sampler = sampler; + (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + + this.yScale = yRadiusScale.scale; + this.xScale = xRadiusScale.scale; + this.extents = new Vector2(xRadiusScale.radius, yRadiusScale.radius); + this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); + this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); + + // We use 2D buffers so that we can access the weight spans in parallel. + this.yBuffer = configuration.MemoryAllocator.Allocate2D(this.yLength, destination.Height); + this.xBuffer = configuration.MemoryAllocator.Allocate2D(this.xLength, destination.Height); + + int maxX = source.Width - 1; + int maxY = source.Height - 1; + this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY); + } + + /// + /// Gets a reference to the first item of the y window. + /// + /// The reference to the first item of the window. + [MethodImpl(InliningOptions.ShortMethod)] + public ref float GetYStartReference(int y) + => ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y)); + + /// + /// Gets a reference to the first item of the x window. + /// + /// The reference to the first item of the window. + [MethodImpl(InliningOptions.ShortMethod)] + public ref float GetXStartReference(int y) + => ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y)); + + public void Convolve( + Vector2 transformedPoint, + int column, + ref float ySpanRef, + ref float xSpanRef, + Buffer2D sourcePixels, + Span targetRow) + where TPixel : struct, IPixel + { + // Clamp sampling pixel radial extents to the source image edges + Vector2 minXY = transformedPoint - this.extents; + Vector2 maxXY = transformedPoint + this.extents; + + // minX, minY, maxX, maxY + var extents = new Vector4( + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F), + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F)); + + int left = (int)extents.X; + int top = (int)extents.Y; + int right = (int)extents.Z; + int bottom = (int)extents.W; + + extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); + + int minX = (int)extents.X; + int minY = (int)extents.Y; + int maxX = (int)extents.Z; + int maxY = (int)extents.W; + + if (minX == maxX || minY == maxY) + { + return; + } + + // TODO: Get Anton to use his superior brain on this one. + // It looks to me like we're calculating the same weights over and over again + // since min(X+Y) and max(X+Y) are the same distance apart. + this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef); + this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef); + + Vector4 sum = Vector4.Zero; + for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++) + { + float yWeight = Unsafe.Add(ref ySpanRef, kernelY); + + for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++) + { + float xWeight = Unsafe.Add(ref xSpanRef, kernelX); + + // Values are first premultiplied to prevent darkening of edge pixels. + var current = sourcePixels[x, y].ToVector4(); + Vector4Utils.Premultiply(ref current); + sum += current * xWeight * yWeight; + } + } + + // Reverse the premultiplication + Vector4Utils.UnPremultiply(ref sum); + targetRow[column] = sum; + } + + /// + /// Calculated the normalized weights for the given point. + /// + /// The minimum sampling offset + /// The maximum sampling offset + /// The length of the weights collection + /// The transformed point dimension + /// The reference to the collection of weights + [MethodImpl(InliningOptions.ShortMethod)] + private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef) + { + float sum = 0; + for (int x = 0, i = min; i <= max; i++, x++) + { + float weight = this.sampler.GetValue(i - point); + sum += weight; + Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point); + } + + // TODO: Do we need this? Check what happens when we scale an image down. + // if (sum > 0) + // { + // for (int i = 0; i < length; i++) + // { + // ref float wRef = ref Unsafe.Add(ref weightsRef, i); + // wRef /= sum; + // } + // } + } + + private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + { + float scale = (float)sourceSize / destinationSize; + + if (scale < 1F) + { + scale = 1F; + } + + return (MathF.Ceiling(scale * this.sampler.Radius), scale); + } + + public void Dispose() + { + this.yBuffer?.Dispose(); + this.xBuffer?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs new file mode 100644 index 000000000..7d0350249 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -0,0 +1,121 @@ +// 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.Processing.Processors.Transforms +{ + /// + /// Contains utility methods for working with transforms. + /// + public static class TransformUtils + { + /// + /// Creates a centered rotation matrix using the given rotation in degrees and the source size. + /// + /// The amount of rotation, in degrees. + /// The source image size. + /// The . + public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + + /// + /// Gets the centered transform matrix based upon the source and destination rectangles. + /// + /// The source image bounds. + /// The transformation matrix. + /// The + public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) + { + Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + + // We invert the matrix to handle the transformation from screen to world space. + // This ensures scaling matrices are correct. + Matrix3x2.Invert(matrix, out Matrix3x2 inverted); + + var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + + // Translate back to world space. + Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); + + return centered; + } + + /// + /// Gets the centered transform matrix based upon the source and destination rectangles. + /// + /// The source image bounds. + /// The destination image bounds. + /// The transformation matrix. + /// The + public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) + { + // We invert the matrix to handle the transformation from screen to world space. + // This ensures scaling matrices are correct. + Matrix3x2.Invert(matrix, out Matrix3x2 inverted); + + var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); + var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + + // Translate back to world space. + Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); + + return centered; + } + + /// + /// Returns the rectangle bounds relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) + { + Rectangle transformed = GetTransformedRectangle(rectangle, matrix); + + // TODO: Check this. + return new Rectangle(0, 0, transformed.Width, transformed.Height); + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) + { + if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) + { + return rectangle; + } + + var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), 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 left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); + float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); + float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); + float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); + + return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); + } + } +} diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 0ec1e295d..3f24969a3 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing /// The public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, source.GetCurrentSize())); + => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, source.GetCurrentSize())); /// /// Transforms an image by the given matrix using the specified sampling algorithm @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing { var t = Matrix3x2.CreateTranslation(-rectangle.Location); Matrix3x2 combinedMatrix = t * matrix; - return source.ApplyProcessor(new AffineTransformProcessor(combinedMatrix, sampler, rectangle.Size)); + return source.ApplyProcessor(new AffineTransformProcessorOld(combinedMatrix, sampler, rectangle.Size)); } /// @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Processing IResampler sampler, Size destinationSize) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(matrix, sampler, destinationSize)); + => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, destinationSize)); /// /// Transforms an image by the given matrix. From 2afd0d572970a1dbbf48e0707ddab23ae86ec648 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 7 Nov 2018 00:00:41 +0000 Subject: [PATCH 15/66] Cleanup TransformKernel and handle negative transform dimensions --- .../Transforms/AffineTransformProcessor.cs | 6 ++++++ .../Processors/Transforms/TransformKernelMap.cs | 15 ++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index edddab181..f63baa95c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -38,6 +38,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // We want to resize the canvas here taking into account any translations. this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); + + // Handle a negative translation that exceeds the original with of the image. + if (this.TargetDimensions.Width <= 0 || this.TargetDimensions.Height <= 0) + { + this.TargetDimensions = sourceSize; + } } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs index 5acc7213c..531edbc45 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -21,8 +21,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { private readonly Buffer2D yBuffer; private readonly Buffer2D xBuffer; - private readonly float yScale; - private readonly float xScale; private readonly int yLength; private readonly int xLength; private readonly Vector2 extents; @@ -39,12 +37,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) { this.sampler = sampler; - (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); - (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + float yRadius = this.GetSamplingRadius(source.Height, destination.Height); + float xRadius = this.GetSamplingRadius(source.Width, destination.Width); - this.yScale = yRadiusScale.scale; - this.xScale = xRadiusScale.scale; - this.extents = new Vector2(xRadiusScale.radius, yRadiusScale.radius); + this.extents = new Vector2(xRadius, yRadius); this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); @@ -167,7 +163,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // } } - private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + [MethodImpl(InliningOptions.ShortMethod)] + private float GetSamplingRadius(int sourceSize, int destinationSize) { float scale = (float)sourceSize / destinationSize; @@ -176,7 +173,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms scale = 1F; } - return (MathF.Ceiling(scale * this.sampler.Radius), scale); + return MathF.Ceiling(scale * this.sampler.Radius); } public void Dispose() From 8205216dfc07f4ea6ebafcbbbbc391c8426173e7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 14:20:52 +0000 Subject: [PATCH 16/66] Introduce AffineTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 177 +++++++++++++ .../Transforms/AffineTransformProcessor.cs | 33 ++- .../Transforms/AffineTransformProcessorOld.cs | 239 ------------------ .../CenteredAffineTransformProcessor.cs | 38 --- .../Processors/Transforms/RotateProcessor.cs | 2 +- .../Processors/Transforms/SkewProcessor.cs | 8 +- .../Processors/Transforms/TransformHelpers.cs | 20 -- .../Transforms/TransformKernelMap.cs | 56 ++-- .../Processors/Transforms/TransformUtils.cs | 78 ++++-- .../Processing/TransformExtensions.cs | 54 +--- .../ImageSharp.Benchmarks/Samplers/Rotate.cs | 10 +- tests/ImageSharp.Benchmarks/Samplers/Skew.cs | 45 ++++ .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 22 +- .../Transforms/AffineTransformTests.cs | 54 ++-- 14 files changed, 360 insertions(+), 476 deletions(-) create mode 100644 src/ImageSharp/Processing/AffineTransformBuilder.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs create mode 100644 tests/ImageSharp.Benchmarks/Samplers/Skew.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs new file mode 100644 index 000000000..e5ce1450f --- /dev/null +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// A helper class for constructing instances for use in affine transforms. + /// + public class AffineTransformBuilder + { + private readonly List matrices = new List(); + private Rectangle rectangle; + + /// + /// Initializes a new instance of the class. + /// + /// The source image size. + public AffineTransformBuilder(Size sourceSize) => this.Size = sourceSize; + + /// + /// Initializes a new instance of the class. + /// + /// The source rectangle. + public AffineTransformBuilder(Rectangle sourceRectangle) + : this(sourceRectangle.Size) + => this.rectangle = sourceRectangle; + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) + { + this.matrices.Insert(0, TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + return this; + } + + /// + /// Gets the source image size. + /// + internal Size Size { get; } + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) + { + this.matrices.Add(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + return this; + } + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScaleMatrix(SizeF scales) + { + this.matrices.Insert(0, Matrix3x2Extensions.CreateScale(scales)); + return this; + } + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScaleMatrix(SizeF scales) + { + this.matrices.Add(Matrix3x2Extensions.CreateScale(scales)); + return this; + } + + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) + { + this.matrices.Insert(0, TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + return this; + } + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) + { + this.matrices.Add(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + return this; + } + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslationMatrix(PointF position) + { + this.matrices.Insert(0, Matrix3x2Extensions.CreateTranslation(position)); + return this; + } + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslationMatrix(PointF position) + { + this.matrices.Add(Matrix3x2Extensions.CreateTranslation(position)); + return this; + } + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// The . + public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) + { + this.matrices.Insert(0, matrix); + return this; + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// The . + public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) + { + this.matrices.Add(matrix); + return this; + } + + /// + /// Returns the combined matrix. + /// + /// The . + public Matrix3x2 BuildMatrix() + { + Matrix3x2 matrix = Matrix3x2.Identity; + + // Translate the origin matrix to cater for source rectangle offsets. + if (!this.rectangle.Equals(default)) + { + matrix *= Matrix3x2.CreateTranslation(-this.rectangle.Location); + } + + foreach (Matrix3x2 m in this.matrices) + { + matrix *= m; + } + + return matrix; + } + + /// + /// Removes all matrices from the builder. + /// + public void Clear() => this.matrices.Clear(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index f63baa95c..fb42b8334 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -19,8 +19,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal class AffineTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { - private readonly Rectangle transformedRectangle; - /// /// Initializes a new instance of the class. /// @@ -32,18 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.transformedRectangle = TransformUtils.GetTransformedRectangle( - new Rectangle(Point.Empty, sourceSize), - matrix); - - // We want to resize the canvas here taking into account any translations. - this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom); - - // Handle a negative translation that exceeds the original with of the image. - if (this.TargetDimensions.Width <= 0 || this.TargetDimensions.Height <= 0) - { - this.TargetDimensions = sourceSize; - } + this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); } /// @@ -79,10 +66,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Rectangle sourceRectangle, Configuration configuration) { + // Handle tranforms that result in output identical to the original. + if (this.TransformMatrix.Equals(Matrix3x2.Identity)) + { + // The cloned will be blank here copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + return; + } + int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - Rectangle sourceBounds = source.Bounds(); var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. @@ -102,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { var point = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(point.X, point.Y)) + if (sourceRectangle.Contains(point.X, point.Y)) { destRow[x] = source[point.X, point.Y]; } @@ -113,7 +107,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return; } - using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler)) + var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler); + try { ParallelHelper.IterateRowsWithTempBuffer( targetBounds, @@ -140,6 +135,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } }); } + finally + { + kernel.Dispose(); + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs deleted file mode 100644 index 5891afd9a..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs +++ /dev/null @@ -1,239 +0,0 @@ -// 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.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.ParallelUtils; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// Provides the base methods to perform affine transforms on an image. - /// - /// The pixel format. - internal class AffineTransformProcessorOld : InterpolatedTransformProcessorBase - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) - { - this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; - } - - /// - /// Gets the matrix used to supply the affine transform - /// - public Matrix3x2 TransformMatrix { get; } - - /// - /// Gets the target dimensions to constrain the transformed image to - /// - public Size TargetDimensions { get; } - - /// - protected override Image CreateDestination(Image source, Rectangle 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(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); - - // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); - } - - /// - protected override void OnFrameApply( - ImageFrame source, - ImageFrame destination, - Rectangle sourceRectangle, - Configuration configuration) - { - 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); - - // Convert from screen to world space. - Matrix3x2.Invert(matrix, out matrix); - - if (this.Sampler is NearestNeighborResampler) - { - ParallelHelper.IterateRows( - targetBounds, - configuration, - rows => - { - for (int y = rows.Min; y < rows.Max; y++) - { - Span destRow = destination.GetPixelRowSpan(y); - - for (int x = 0; x < width; x++) - { - var point = Point.Transform(new Point(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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) - { - ParallelHelper.IterateRows( - targetBounds, - configuration, - rows => - { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(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. - // Precalculating 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } - } - }); - } - } - - /// - /// 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 Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => this.TransformMatrix; - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs deleted file mode 100644 index 82614dc8c..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs +++ /dev/null @@ -1,38 +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.Transforms -{ - /// - /// A base class that provides methods to allow the automatic centering of affine transforms - /// - /// The pixel format. - internal abstract class CenteredAffineTransformProcessor : AffineTransformProcessorOld - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The source image size - protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) - : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) - { - } - - /// - protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - => TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); - - private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) - { - var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; - } - } -} \ 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 cbf82cc9b..57cca4bf9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : base( - TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize), + TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index a0cfa6379..4a006a9df 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -11,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides methods that allow the skewing of images. /// /// The pixel format. - internal class SkewProcessor : CenteredAffineTransformProcessor + internal class SkewProcessor : AffineTransformProcessor where TPixel : struct, IPixel { /// @@ -33,7 +32,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the skew operation. /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) - : base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler, sourceSize) + : base( + TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize), + sampler, + sourceSize) { this.DegreesX = degreesX; this.DegreesY = degreesY; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs index b22fa64cf..2e85f6c2c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs @@ -102,26 +102,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return centered; } - /// - /// Returns the bounding rectangle 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); - - return GetBoundingRectangle(tl, tr, bl, br); - } - /// /// Returns the bounding rectangle relative to the source for the given transformation matrix. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs index 531edbc45..573120888 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs @@ -9,20 +9,15 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; -// TODO: It would be great if we could somehow optimize this to calculate the weights once. -// currently we cannot do that as we are calulating the weight of the transformed point dimension -// not the point in the original image. namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Contains the methods required to calculate kernel sampling weights on-the-fly. + /// Contains the methods required to calculate transform kernel convolution. /// internal class TransformKernelMap : IDisposable { private readonly Buffer2D yBuffer; private readonly Buffer2D xBuffer; - private readonly int yLength; - private readonly int xLength; private readonly Vector2 extents; private Vector4 maxSourceExtents; private readonly IResampler sampler; @@ -41,12 +36,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float xRadius = this.GetSamplingRadius(source.Width, destination.Width); this.extents = new Vector2(xRadius, yRadius); - this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); - this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); + int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); + int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); - // We use 2D buffers so that we can access the weight spans in parallel. - this.yBuffer = configuration.MemoryAllocator.Allocate2D(this.yLength, destination.Height); - this.xBuffer = configuration.MemoryAllocator.Allocate2D(this.xLength, destination.Height); + // We use 2D buffers so that we can access the weight spans per row in parallel. + this.yBuffer = configuration.MemoryAllocator.Allocate2D(yLength, destination.Height); + this.xBuffer = configuration.MemoryAllocator.Allocate2D(xLength, destination.Height); int maxX = source.Width - 1; int maxY = source.Height - 1; @@ -82,42 +77,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Vector2 minXY = transformedPoint - this.extents; Vector2 maxXY = transformedPoint + this.extents; - // minX, minY, maxX, maxY + // left, top, right, bottom var extents = new Vector4( MathF.Ceiling(minXY.X - .5F), MathF.Ceiling(minXY.Y - .5F), MathF.Floor(maxXY.X + .5F), MathF.Floor(maxXY.Y + .5F)); + extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); + int left = (int)extents.X; int top = (int)extents.Y; int right = (int)extents.Z; int bottom = (int)extents.W; - extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); - - int minX = (int)extents.X; - int minY = (int)extents.Y; - int maxX = (int)extents.Z; - int maxY = (int)extents.W; - - if (minX == maxX || minY == maxY) + if (left == right || top == bottom) { return; } - // TODO: Get Anton to use his superior brain on this one. - // It looks to me like we're calculating the same weights over and over again - // since min(X+Y) and max(X+Y) are the same distance apart. - this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef); - this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef); + this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef); + this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef); Vector4 sum = Vector4.Zero; - for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++) + for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++) { float yWeight = Unsafe.Add(ref ySpanRef, kernelY); - for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++) + for (int kernelX = 0, x = left; x <= right; x++, kernelX++) { float xWeight = Unsafe.Add(ref xSpanRef, kernelX); @@ -138,29 +125,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The minimum sampling offset /// The maximum sampling offset - /// The length of the weights collection /// The transformed point dimension /// The reference to the collection of weights [MethodImpl(InliningOptions.ShortMethod)] - private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef) + private void CalculateWeights(int min, int max, float point, ref float weightsRef) { float sum = 0; for (int x = 0, i = min; i <= max; i++, x++) { float weight = this.sampler.GetValue(i - point); sum += weight; - Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point); + Unsafe.Add(ref weightsRef, x) = weight; } - - // TODO: Do we need this? Check what happens when we scale an image down. - // if (sum > 0) - // { - // for (int i = 0; i < length; i++) - // { - // ref float wRef = ref Unsafe.Add(ref weightsRef, i); - // wRef /= sum; - // } - // } } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 7d0350249..10cf49c34 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Contains utility methods for working with transforms. /// - public static class TransformUtils + internal static class TransformUtils { /// /// Creates a centered rotation matrix using the given rotation in degrees and the source size. @@ -18,43 +18,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The amount of rotation, in degrees. /// The source image size. /// The . - public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size) + public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size) => CreateCenteredTransformMatrix( new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); /// - /// Gets the centered transform matrix based upon the source and destination rectangles. + /// Creates a centered skew matrix from the give angles in degrees and the source size. /// - /// The source image bounds. - /// The transformation matrix. - /// The - public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) - { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); - - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); - - // Translate back to world space. - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - return centered; - } + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The source image size. + /// The . + public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); /// /// Gets the centered transform matrix based upon the source and destination rectangles. /// /// The source image bounds. - /// The destination image bounds. /// The transformation matrix. /// The - public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) + public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) { + Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. Matrix3x2.Invert(matrix, out Matrix3x2 inverted); @@ -79,8 +69,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) { Rectangle transformed = GetTransformedRectangle(rectangle, matrix); - - // TODO: Check this. return new Rectangle(0, 0, transformed.Width, transformed.Height); } @@ -107,6 +95,44 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return GetBoundingRectangle(tl, tr, bl, br); } + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The source size. + /// The transformation matrix. + /// + /// The . + /// + public static Size GetTransformedSize(Size size, Matrix3x2 matrix) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) + { + return size; + } + + Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + + // We want to resize the canvas here taking into account any translations. + int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); + int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); + + // If location in either direction is translated to a negative value equal to or exceeding the + // dimensions in eith direction we need to reassign the dimension. + if (height <= 0) + { + height = rectangle.Height; + } + + if (width <= 0) + { + width = rectangle.Width; + } + + return new Size(width, height); + } + private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) { // Find the minimum and maximum "corners" based on the given vectors diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 3f24969a3..4cbd1b041 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -18,65 +18,23 @@ namespace SixLabors.ImageSharp.Processing /// /// The pixel format. /// The image to transform. - /// The transformation matrix. + /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix) + public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder) where TPixel : struct, IPixel - => Transform(source, matrix, KnownResamplers.Bicubic); + => Transform(source, builder, KnownResamplers.Bicubic); /// /// 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 - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) - where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, source.GetCurrentSize())); - - /// - /// 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 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 rectangle) - where TPixel : struct, IPixel - { - var t = Matrix3x2.CreateTranslation(-rectangle.Location); - Matrix3x2 combinedMatrix = t * matrix; - return source.ApplyProcessor(new AffineTransformProcessorOld(combinedMatrix, sampler, rectangle.Size)); - } - - /// - /// 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 affine transform builder. /// The to perform the resampling. - /// The size of the destination image. /// The - public static IImageProcessingContext Transform( - this IImageProcessingContext source, - Matrix3x2 matrix, - IResampler sampler, - Size destinationSize) + public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessorOld(matrix, sampler, destinationSize)); + => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); /// /// Transforms an image by the given matrix. diff --git a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs index c1456f9d7..f898576af 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Rotate.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers } } -// Nov 4 2018 +// Nov 7 2018 //BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores //.NET Core SDK = 2.1.403 @@ -36,4 +36,10 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers // Method | Runtime | Mean | Error | StdDev | Allocated | //--------- |-------- |---------:|----------:|----------:|----------:| // DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB | -// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | \ No newline at end of file +// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB | + +// #### AFTER ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//--------- |-------- |---------:|---------:|---------:|----------:| +// DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB | +// DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB | \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Samplers/Skew.cs b/tests/ImageSharp.Benchmarks/Samplers/Skew.cs new file mode 100644 index 000000000..84819750a --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Samplers/Skew.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Benchmarks.Samplers +{ + [Config(typeof(Config.ShortClr))] + public class Skew + { + [Benchmark] + public Size DoSkew() + { + using (var image = new Image(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond)) + { + image.Mutate(x => x.Skew(20, 10)); + + return image.Size(); + } + } + } +} + +// Nov 7 2018 +//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763 +//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores +//.NET Core SDK = 2.1.403 + +// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT +// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 +// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT + +//LaunchCount=1 TargetCount=3 WarmupCount=3 + +// #### BEFORE ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//------- |-------- |---------:|---------:|----------:|----------:| +// DoSkew | Clr | 78.14 ms | 8.383 ms | 0.4736 ms | 6 KB | +// DoSkew | Core | 44.22 ms | 4.109 ms | 0.2322 ms | 4.28 KB | + +// #### AFTER ####: +//Method | Runtime | Mean | Error | StdDev | Allocated | +//------- |-------- |---------:|----------:|----------:|----------:| +// DoSkew | Clr | 71.63 ms | 25.589 ms | 1.4458 ms | 6 KB | +// DoSkew | Core | 38.99 ms | 8.640 ms | 0.4882 ms | 4.36 KB | \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 496692d96..564318e5e 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -2,10 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; @@ -75,21 +73,15 @@ namespace SixLabors.ImageSharp.Tests using (Image image = provider.GetImage()) using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { - Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F); - Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F)); - Matrix3x2 matrix = rotate * scale; + AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) + .AppendRotateMatrixDegrees(45F) + .AppendScaleMatrix(new SizeF(.25F, .25F)) + .AppendTranslationMatrix(new PointF(10, 10)); - // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor - Rectangle srcBounds = blend.Bounds(); - Rectangle destBounds = TransformHelpers.GetTransformedBoundingRectangle(srcBounds, matrix); - Matrix3x2 centeredMatrix = TransformHelpers.GetCenteredTransformMatrix(srcBounds, destBounds, matrix); - - // We pass a new rectangle here based on the dest bounds since we've offset the matrix - blend.Mutate(x => x.Transform( - centeredMatrix, - KnownResamplers.Bicubic, - new Rectangle(0, 0, destBounds.Width, destBounds.Height))); + // Apply a background color so we can see the translation. + blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors.HotPink)); + // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); image.Mutate(x => x.DrawImage(blend, position, mode, .75F)); image.DebugSave(provider, new[] { "Transformed" }); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index ae572498a..32280d48c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -78,15 +78,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler resampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F)); - var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees((float)Math.PI / 4F); - Rectangle sourceRectangle = image.Bounds(); - Matrix3x2 matrix = rotate * translate; - - Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix); - - image.Mutate(c => c.Transform(matrix, resampler, destRectangle)); + image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); VerifyAllPixelsAreWhiteOrTransparent(image); @@ -104,14 +99,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { 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; + image.DebugSave(provider, $"_original"); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(angleDeg) + .AppendScaleMatrix(new SizeF(sx, sy)) + .AppendTranslationMatrix(new PointF(tx, ty)); - this.PrintMatrix(m); + this.PrintMatrix(builder.BuildMatrix()); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"; image.DebugSave(provider, testOutputDetails); @@ -126,9 +122,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(angleDeg) + .AppendScaleMatrix(new SizeF(s, s)); - image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({s})"; image.DebugSave(provider, testOutputDetails); @@ -155,13 +153,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void Transform_FromSourceRectangle1(TestImageProvider provider) where TPixel : struct, IPixel { - var rectangle = new Rectangle(48, 0, 96, 36); + var rectangle = new Rectangle(48, 0, 48, 24); using (Image image = provider.GetImage()) { - var m = Matrix3x2.CreateScale(2.0F, 1.5F); + image.DebugSave(provider, $"_original"); + AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + .AppendScaleMatrix(new SizeF(2, 1.5F)); - image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -173,13 +173,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void Transform_FromSourceRectangle2(TestImageProvider provider) where TPixel : struct, IPixel { - var rectangle = new Rectangle(0, 24, 48, 48); + var rectangle = new Rectangle(0, 24, 48, 24); using (Image image = provider.GetImage()) { - var m = Matrix3x2.CreateScale(1.0F, 2.0F); + AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + .AppendScaleMatrix(new SizeF(1F, 2F)); - image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); + image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -194,12 +195,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image); + AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + .AppendRotateMatrixDegrees(50) + .AppendScaleMatrix(new SizeF(.6F, .6F)); - image.Mutate(i => - { - i.Transform(m, sampler); - }); + image.Mutate(i => i.Transform(builder, sampler)); image.DebugSave(provider, resamplerName); image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); From 5589bd3e39007710b40d4f380690003a81209485 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 21:14:30 +0000 Subject: [PATCH 17/66] Introduce ProjectiveTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 10 +- .../Transforms/AffineTransformProcessor.cs | 6 +- .../CenteredProjectiveTransformProcessor.cs | 40 ---- .../InterpolatedTransformProcessorBase.cs | 116 ----------- .../ProjectiveTransformProcessor.cs | 197 +++++------------- .../Processors/Transforms/TransformHelpers.cs | 138 ------------ .../Transforms/TransformProcessorBase.cs | 2 +- .../Transforms/TransformProcessorHelpers.cs | 58 ++++++ .../Processors/Transforms/TransformUtils.cs | 162 ++++++++++++++ .../Processing/ProjectiveTransformBuilder.cs | 113 ++++++++++ .../Processing/ProjectiveTransformHelper.cs | 166 --------------- src/ImageSharp/Processing/TaperCorner.cs | 26 +++ src/ImageSharp/Processing/TaperSide.cs | 31 +++ .../Processing/TransformExtensions.cs | 40 +--- .../Transforms/AffineTransformTests.cs | 2 +- .../Transforms/ProjectiveTransformTests.cs | 27 +-- .../Transforms/TransformsHelpersTest.cs | 2 +- 17 files changed, 475 insertions(+), 661 deletions(-) delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs create mode 100644 src/ImageSharp/Processing/ProjectiveTransformBuilder.cs delete mode 100644 src/ImageSharp/Processing/ProjectiveTransformHelper.cs create mode 100644 src/ImageSharp/Processing/TaperCorner.cs create mode 100644 src/ImageSharp/Processing/TaperSide.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index e5ce1450f..ff44915b1 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Processing : this(sourceRectangle.Size) => this.rectangle = sourceRectangle; + /// + /// Gets the source image size. + /// + internal Size Size { get; } + /// /// Prepends a centered rotation matrix using the given rotation in degrees. /// @@ -41,11 +46,6 @@ namespace SixLabors.ImageSharp.Processing return this; } - /// - /// Gets the source image size. - /// - internal Size Size { get; } - /// /// Appends a centered rotation matrix using the given rotation in degrees. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index fb42b8334..2370adb86 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Initializes a new instance of the class. /// - /// The transform matrix + /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size + /// The source image size. public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) { Guard.NotNull(sampler, nameof(sampler)); @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Configuration configuration) { // Handle tranforms that result in output identical to the original. - if (this.TransformMatrix.Equals(Matrix3x2.Identity)) + if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity)) { // The cloned will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); diff --git a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs deleted file mode 100644 index 962b9e4c9..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs +++ /dev/null @@ -1,40 +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.Transforms -{ - /// - /// A base class that provides methods to allow the automatic centering of non-affine transforms - /// - /// The pixel format. - internal abstract class CenteredProjectiveTransformProcessor : ProjectiveTransformProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The transform matrix - /// The sampler to perform the transform operation. - /// The source image size - protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) - : base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix)) - { - } - - /// - protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) - { - return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix); - } - - private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix) - { - var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); - return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs deleted file mode 100644 index 4737a4102..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// The base class for performing interpolated affine and non-affine transforms. - /// - /// The pixel format. - internal abstract class InterpolatedTransformProcessorBase : TransformProcessorBase - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The sampler to perform the transform operation. - protected InterpolatedTransformProcessorBase(IResampler sampler) - { - Guard.NotNull(sampler, nameof(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 normalized. - /// - /// 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 reference to the collection of weights - /// The length of the weights collection - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length) - { - float sum = 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 weightsRef, x) = weight; - } - - if (sum > 0) - { - for (int i = 0; i < length; i++) - { - ref float wRef = ref Unsafe.Add(ref weightsRef, i); - wRef /= sum; - } - } - } - - /// - /// Calculated the weights for the given point. - /// - /// The minimum source bounds - /// The maximum source bounds - /// The transformed point dimension - /// The sampler - /// The reference to the collection of weights - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef) - { - for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) - { - Unsafe.Add(ref weightsRef, x) = sampler.GetValue(i - point); - } - } - - /// - /// 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); - } - } -} \ 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 50af26aeb..bfde1769c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -5,13 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -20,22 +16,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Provides the base methods to perform non-affine transforms on an image. /// /// The pixel format. - internal class ProjectiveTransformProcessor : InterpolatedTransformProcessorBase + internal class ProjectiveTransformProcessor : TransformProcessorBase where TPixel : struct, IPixel { /// /// Initializes a new instance of the class. /// - /// The transform matrix + /// The transform matrix. /// The sampler to perform the transform operation. - /// The target dimensions to constrain the transformed image to. - public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions) - : base(sampler) + /// The source image size. + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) { + Guard.NotNull(sampler, nameof(sampler)); + this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions; + this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); } + /// + /// Gets the sampler to perform interpolation of the transform operation. + /// + public IResampler Sampler { get; } + /// /// Gets the matrix used to supply the projective transform /// @@ -60,17 +62,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// protected override void OnFrameApply(ImageFrame source, ImageFrame destination, Rectangle sourceRectangle, Configuration configuration) { + // Handle tranforms that result in output identical to the original. + if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity)) + { + // The cloned will be blank here copy all the pixel data over + source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + return; + } + 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 - Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); + var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. - Matrix4x4.Invert(matrix, out matrix); + Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 matrix); const float Epsilon = 0.0000001F; if (this.Sampler is NearestNeighborResampler) @@ -92,7 +98,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int px = (int)MathF.Round(v3.X / z); int py = (int)MathF.Round(v3.Y / z); - if (sourceBounds.Contains(px, py)) + if (sourceRectangle.Contains(px, py)) { destRow[x] = source[px, py]; } @@ -103,145 +109,40 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms 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; - - // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: - var radius = new Vector4(xRadiusScale.radius, yRadiusScale.radius, 0, 0); - - 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); - - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - - using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) - using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) + var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler); + try { - ParallelHelper.IterateRows( + ParallelHelper.IterateRowsWithTempBuffer( targetBounds, configuration, - rows => + (rows, vectorBuffer) => + { + Span vectorSpan = vectorBuffer.Span; + for (int y = rows.Min; y < rows.Max; y++) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(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 v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); - float z = MathF.Max(v3.Z, Epsilon); - - // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: - Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z; - - // Clamp sampling pixel radial extents to the source image edges - Vector4 maxXY = point + radius; - Vector4 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; + Span targetRowSpan = destination.GetPixelRowSpan(y); + PixelOperations.Instance.ToVector4(configuration, targetRowSpan, vectorSpan); + ref float ySpanRef = ref kernel.GetYStartReference(y); + ref float xSpanRef = ref kernel.GetXStartReference(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; - - 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, - ref ySpanRef, - yLength); - - CalculateWeightsDown( - left, - right, - minX, - maxX, - point.X, - sampler, - xScale, - ref xSpanRef, - xLength); - } - else - { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } - - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) - { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - - // Values are first premultiplied to prevent darkening of edge pixels - var current = source[i, j].ToVector4(); - Vector4Utils.Premultiply(ref current); - sum += current * xWeight * yWeight; - } - } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + 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 v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); - // Reverse the premultiplication - Vector4Utils.UnPremultiply(ref sum); - dest.FromVector4(sum); - } + kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); } - }); + + PixelOperations.Instance.FromVector4(configuration, vectorSpan, targetRowSpan); + } + }); + } + finally + { + kernel.Dispose(); } } - - /// - /// 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) => this.TransformMatrix; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs deleted file mode 100644 index 2e85f6c2c..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using SixLabors.ImageSharp.MetaData.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// Contains helper methods for working with affine and non-affine transforms - /// - internal static 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 is 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); - - 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); - - if (image.Height <= ushort.MaxValue) - { - profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); - } - else - { - profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); - } - } - } - - /// - /// Gets the centered transform matrix based upon the source and destination rectangles - /// - /// The source image bounds. - /// The destination image bounds. - /// The transformation matrix. - /// The - public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) - { - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); - var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); - - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - // Translate back to world to pass to the Transform method. - return centered; - } - - /// - /// Gets the centered transform matrix based upon the source and destination rectangles - /// - /// The source image bounds. - /// The destination image bounds. - /// The transformation matrix. - /// The - public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix) - { - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix4x4.Invert(matrix, out Matrix4x4 inverted); - - var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); - var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0); - - Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered); - - // Translate back to world to pass to the Transform method. - return centered; - } - - /// - /// Returns the bounding rectangle 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))); - 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/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs index 13ee90a06..4973b90f4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs @@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// protected override void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) - => TransformHelpers.UpdateDimensionalMetData(destination); + => TransformProcessorHelpers.UpdateDimensionalMetData(destination); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs new file mode 100644 index 000000000..f5536d046 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Contains helper methods for working with transforms. + /// + internal static class TransformProcessorHelpers + { + /// + /// 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 is 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); + + 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); + + if (image.Height <= ushort.MaxValue) + { + profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height); + } + else + { + profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 10cf49c34..f561d3513 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -58,6 +58,111 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return centered; } + /// + /// Creates a matrix that performs a tapering projective transform. + /// + /// + /// The rectangular size of the image being transformed. + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The + public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction) + { + Matrix4x4 matrix = Matrix4x4.Identity; + + switch (side) + { + case TaperSide.Left: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M13 = (fraction - 1) / size.Width; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + matrix.M32 = size.Height * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M13; + matrix.M32 = size.Height * (1 - fraction) / 2; + break; + } + + break; + + case TaperSide.Top: + matrix.M11 = fraction; + matrix.M22 = fraction; + matrix.M23 = (fraction - 1) / size.Height; + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + matrix.M31 = size.Width * (1 - fraction); + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M23; + matrix.M31 = size.Width * (1 - fraction) / 2; + break; + } + + break; + + case TaperSide.Right: + matrix.M11 = 1 / fraction; + matrix.M13 = (1 - fraction) / (size.Width * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + break; + + case TaperCorner.Both: + matrix.M12 = size.Height * .5F * matrix.M13; + break; + } + + break; + + case TaperSide.Bottom: + matrix.M22 = 1 / fraction; + matrix.M23 = (1 - fraction) / (size.Height * fraction); + + switch (corner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + break; + + case TaperCorner.Both: + matrix.M21 = size.Width * .5F * matrix.M23; + break; + } + + break; + } + + return matrix; + } + /// /// Returns the rectangle bounds relative to the source for the given transformation matrix. /// @@ -114,6 +219,63 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + return ConstrainSize(rectangle); + } + + /// + /// Returns the rectangle relative to the source for the given transformation matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) + { + if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + { + return rectangle; + } + + Vector2 GetVector(float x, float y) + { + const float Epsilon = 0.0000001F; + var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); + return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); + } + + Vector2 tl = GetVector(rectangle.Left, rectangle.Top); + Vector2 tr = GetVector(rectangle.Right, rectangle.Top); + Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom); + Vector2 br = GetVector(rectangle.Right, rectangle.Bottom); + + return GetBoundingRectangle(tl, tr, bl, br); + } + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// + /// The source size. + /// The transformation matrix. + /// + /// The . + /// + public static Size GetTransformedSize(Size size, Matrix4x4 matrix) + { + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + { + return size; + } + + Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + + return ConstrainSize(rectangle); + } + + private static Size ConstrainSize(Rectangle rectangle) + { // We want to resize the canvas here taking into account any translations. int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs new file mode 100644 index 000000000..3edc993c4 --- /dev/null +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// A helper class for constructing instances for use in projective transforms. + /// + public class ProjectiveTransformBuilder + { + private readonly List matrices = new List(); + private Rectangle rectangle; + + /// + /// Initializes a new instance of the class. + /// + /// The source image size. + public ProjectiveTransformBuilder(Size sourceSize) => this.Size = sourceSize; + + /// + /// Initializes a new instance of the class. + /// + /// The source rectangle. + public ProjectiveTransformBuilder(Rectangle sourceRectangle) + : this(sourceRectangle.Size) + => this.rectangle = sourceRectangle; + + /// + /// Gets the source image size. + /// + internal Size Size { get; } + + /// + /// Prepends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + { + this.matrices.Insert(0, TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + return this; + } + + /// + /// Appends a matrix that performs a tapering projective transform. + /// + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The . + public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + { + this.matrices.Add(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + return this; + } + + /// + /// Prepends a raw matrix. + /// + /// The matrix to prepend. + /// The . + public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) + { + this.matrices.Insert(0, matrix); + return this; + } + + /// + /// Appends a raw matrix. + /// + /// The matrix to append. + /// The . + public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) + { + this.matrices.Add(matrix); + return this; + } + + /// + /// Returns the combined matrix. + /// + /// The . + public Matrix4x4 BuildMatrix() + { + Matrix4x4 matrix = Matrix4x4.Identity; + + // Translate the origin matrix to cater for source rectangle offsets. + if (!this.rectangle.Equals(default)) + { + matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.rectangle.Location, 0)); + } + + foreach (Matrix4x4 m in this.matrices) + { + matrix *= m; + } + + return matrix; + } + + /// + /// Removes all matrices from the builder. + /// + public void Clear() => this.matrices.Clear(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/ProjectiveTransformHelper.cs b/src/ImageSharp/Processing/ProjectiveTransformHelper.cs deleted file mode 100644 index 4057ec586..000000000 --- a/src/ImageSharp/Processing/ProjectiveTransformHelper.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System.Numerics; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Enumerates the various options which determine which side to taper - /// - public enum TaperSide - { - /// - /// Taper the left side - /// - Left, - - /// - /// Taper the top side - /// - Top, - - /// - /// Taper the right side - /// - Right, - - /// - /// Taper the bottom side - /// - Bottom - } - - /// - /// Enumerates the various options which determine how to taper corners - /// - public enum TaperCorner - { - /// - /// Taper the left or top corner - /// - LeftOrTop, - - /// - /// Taper the right or bottom corner - /// - RightOrBottom, - - /// - /// Taper the both sets of corners - /// - Both - } - - /// - /// Provides helper methods for working with generalized projective transforms. - /// - public static class ProjectiveTransformHelper - { - /// - /// Creates a matrix that performs a tapering projective transform. - /// - /// - /// The rectangular size of the image being transformed. - /// An enumeration that indicates the side of the rectangle that tapers. - /// An enumeration that indicates on which corners to taper the rectangle. - /// The amount to taper. - /// The - public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction) - { - Matrix4x4 matrix = Matrix4x4.Identity; - - switch (taperSide) - { - case TaperSide.Left: - matrix.M11 = taperFraction; - matrix.M22 = taperFraction; - matrix.M13 = (taperFraction - 1) / size.Width; - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; - matrix.M32 = size.Height * (1 - taperFraction); - break; - - case TaperCorner.Both: - matrix.M12 = (size.Height * 0.5f) * matrix.M13; - matrix.M32 = size.Height * (1 - taperFraction) / 2; - break; - } - - break; - - case TaperSide.Top: - matrix.M11 = taperFraction; - matrix.M22 = taperFraction; - matrix.M23 = (taperFraction - 1) / size.Height; - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; - matrix.M31 = size.Width * (1 - taperFraction); - break; - - case TaperCorner.Both: - matrix.M21 = (size.Width * 0.5f) * matrix.M23; - matrix.M31 = size.Width * (1 - taperFraction) / 2; - break; - } - - break; - - case TaperSide.Right: - matrix.M11 = 1 / taperFraction; - matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction); - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; - break; - - case TaperCorner.Both: - matrix.M12 = (size.Height * 0.5f) * matrix.M13; - break; - } - - break; - - case TaperSide.Bottom: - matrix.M22 = 1 / taperFraction; - matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction); - - switch (taperCorner) - { - case TaperCorner.RightOrBottom: - break; - - case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; - break; - - case TaperCorner.Both: - matrix.M21 = (size.Width * 0.5f) * matrix.M23; - break; - } - - break; - } - - return matrix; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TaperCorner.cs b/src/ImageSharp/Processing/TaperCorner.cs new file mode 100644 index 000000000..395b17142 --- /dev/null +++ b/src/ImageSharp/Processing/TaperCorner.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Enumerates the various options which determine how to taper corners + /// + public enum TaperCorner + { + /// + /// Taper the left or top corner + /// + LeftOrTop, + + /// + /// Taper the right or bottom corner + /// + RightOrBottom, + + /// + /// Taper the both sets of corners + /// + Both + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TaperSide.cs b/src/ImageSharp/Processing/TaperSide.cs new file mode 100644 index 000000000..226d11aed --- /dev/null +++ b/src/ImageSharp/Processing/TaperSide.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Enumerates the various options which determine which side to taper + /// + public enum TaperSide + { + /// + /// Taper the left side + /// + Left, + + /// + /// Taper the top side + /// + Top, + + /// + /// Taper the right side + /// + Right, + + /// + /// Taper the bottom side + /// + Bottom + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index 4cbd1b041..ea789eb3d 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -1,10 +1,8 @@ // 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.Transforms; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { @@ -14,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing public static class TransformExtensions { /// - /// Transforms an image by the given matrix. + /// Performs an affine transform of an image. /// /// The pixel format. /// The image to transform. @@ -25,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Transforms an image by the given matrix using the specified sampling algorithm. + /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. /// The image to transform. @@ -37,44 +35,26 @@ namespace SixLabors.ImageSharp.Processing => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); /// - /// Transforms an image by the given matrix. + /// Performs a projective transform of an image. /// /// The pixel format. /// The image to transform. - /// The transformation matrix. - /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix) - where TPixel : struct, IPixel - => Transform(source, matrix, KnownResamplers.Bicubic); - - /// - /// Applies a projective transform to the 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 affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) + public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder) where TPixel : struct, IPixel - => source.ApplyProcessor(new ProjectiveTransformProcessor(matrix, sampler, source.GetCurrentSize())); + => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. - /// TODO: Should we be offsetting the matrix here? + /// Performs a projective transform of an image using the specified sampling algorithm. /// /// The pixel format. /// The image to transform. - /// The transformation matrix. + /// The projective transform builder. /// 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) + public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - { - var t = Matrix4x4.CreateTranslation(new Vector3(-rectangle.Location, 0)); - Matrix4x4 combinedMatrix = t * matrix; - return source.ApplyProcessor(new ProjectiveTransformProcessor(combinedMatrix, sampler, rectangle.Size)); - } + => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 32280d48c..e05a8e381 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees((float)Math.PI / 4F); + .AppendRotateMatrixDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 5190a71e7..0362d75a0 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); private ITestOutputHelper Output { get; } - + public static readonly TheoryData ResamplerNames = new TheoryData { nameof(KnownResamplers.Bicubic), @@ -60,10 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }; - public ProjectiveTransformTests(ITestOutputHelper output) - { - this.Output = output; - } + public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output; [Theory] [WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] @@ -73,9 +70,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F); + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendTaperMatrix(TaperSide.Right, TaperCorner.Both, .5F); - image.Mutate(i => { i.Transform(m, sampler); }); + image.Mutate(i => i.Transform(builder, sampler)); image.DebugSave(provider, resamplerName); image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); @@ -89,8 +87,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F); - image.Mutate(i => { i.Transform(m); }); + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendTaperMatrix(taperSide, taperCorner, .5F); + + image.Mutate(i => i.Transform(builder)); FormattableString testOutputDetails = $"{taperSide}-{taperCorner}"; image.DebugSave(provider, testOutputDetails); @@ -110,10 +110,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine using (Image image = provider.GetImage()) { - Matrix4x4 m = Matrix4x4.Identity; - m.M13 = 0.01F; + Matrix4x4 matrix = Matrix4x4.Identity; + matrix.M13 = 0.01F; + + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + .AppendMatrix(matrix); - image.Mutate(i => { i.Transform(m); }); + image.Mutate(i => i.Transform(builder)); image.DebugSave(provider); image.CompareToReferenceOutput(TolerantComparer, provider); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs index 146ed6230..909e50535 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType); Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType); - TransformHelpers.UpdateDimensionalMetData(img); + TransformProcessorHelpers.UpdateDimensionalMetData(img); Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType); Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType); From 8b0e276de532d4900b722c75099bb3f9d20235ff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 10 Nov 2018 21:45:02 +0000 Subject: [PATCH 18/66] Update tests/Images/External --- tests/Images/External | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Images/External b/tests/Images/External index ed8a7b0b6..e7e0f1311 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit ed8a7b0b6fe1b2e2a7c822aa617103ae31192655 +Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4 From 15093ba8bff157d0daac92e61e87333779eaa91e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 10:24:32 +0000 Subject: [PATCH 19/66] Change tolerance for 32bit framework builds. --- .../Processing/Transforms/AffineTransformTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index e05a8e381..15b4500bb 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -17,7 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { private readonly ITestOutputHelper Output; - private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0085f, 3); + // 1 byte difference on one color component. + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0134F, 3); /// /// angleDeg, sx, sy, tx, ty From 13b9e9c596dc6ca02e1f4c52b6bb6607791ab133 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 21:33:39 +0000 Subject: [PATCH 20/66] Improve transform builder coverage --- .../Processing/AffineTransformBuilder.cs | 16 ++++++++-------- .../Processing/ProjectiveTransformBuilder.cs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ff44915b1..b2b011264 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) { - this.matrices.Insert(0, TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); return this; } @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) { - this.matrices.Add(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); return this; } @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependScaleMatrix(SizeF scales) { - this.matrices.Insert(0, Matrix3x2Extensions.CreateScale(scales)); + this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); return this; } @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendScaleMatrix(SizeF scales) { - this.matrices.Add(Matrix3x2Extensions.CreateScale(scales)); + this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); return this; } @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) { - this.matrices.Insert(0, TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); return this; } @@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) { - this.matrices.Add(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); return this; } @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependTranslationMatrix(PointF position) { - this.matrices.Insert(0, Matrix3x2Extensions.CreateTranslation(position)); + this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); return this; } @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendTranslationMatrix(PointF position) { - this.matrices.Add(Matrix3x2Extensions.CreateTranslation(position)); + this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); return this; } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 3edc993c4..529dd56b1 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) { - this.matrices.Insert(0, TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); return this; } @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) { - this.matrices.Add(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); return this; } From 8a47a1894b5d312fb697b1d49d52556fcd63c526 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 11 Nov 2018 22:42:05 +0000 Subject: [PATCH 21/66] Cleanup and add dedicated transform builder tests --- .../Processing/AffineTransformBuilder.cs | 48 ++++------- .../Processing/ProjectiveTransformBuilder.cs | 18 ++--- .../Transforms/AffineTransformBuilderTests.cs | 79 +++++++++++++++++++ .../ProjectiveTransformBuilderTests.cs | 78 ++++++++++++++++++ 4 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index b2b011264..003249d6e 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -20,7 +20,13 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The source image size. - public AffineTransformBuilder(Size sourceSize) => this.Size = sourceSize; + public AffineTransformBuilder(Size sourceSize) + { + Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); + Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); + + this.Size = sourceSize; + } /// /// Initializes a new instance of the class. @@ -41,10 +47,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) - { - this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -52,10 +55,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - { - this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// /// Prepends a scale matrix from the given vector scale. @@ -63,10 +63,7 @@ namespace SixLabors.ImageSharp.Processing /// The horizontal and vertical scale. /// The . public AffineTransformBuilder PrependScaleMatrix(SizeF scales) - { - this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); - return this; - } + => this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); /// /// Appends a scale matrix from the given vector scale. @@ -74,10 +71,7 @@ namespace SixLabors.ImageSharp.Processing /// The horizontal and vertical scale. /// The . public AffineTransformBuilder AppendScaleMatrix(SizeF scales) - { - this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); - return this; - } + => this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -86,10 +80,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) - { - this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// /// Appends a centered skew matrix from the give angles in degrees. @@ -98,10 +89,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) - { - this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// /// Prepends a translation matrix from the given vector. @@ -109,10 +97,7 @@ namespace SixLabors.ImageSharp.Processing /// The translation position. /// The . public AffineTransformBuilder PrependTranslationMatrix(PointF position) - { - this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); - return this; - } + => this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. @@ -120,10 +105,7 @@ namespace SixLabors.ImageSharp.Processing /// The translation position. /// The . public AffineTransformBuilder AppendTranslationMatrix(PointF position) - { - this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); - return this; - } + => this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); /// /// Prepends a raw matrix. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 529dd56b1..be4c67518 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -20,7 +20,13 @@ namespace SixLabors.ImageSharp.Processing /// Initializes a new instance of the class. /// /// The source image size. - public ProjectiveTransformBuilder(Size sourceSize) => this.Size = sourceSize; + public ProjectiveTransformBuilder(Size sourceSize) + { + Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); + Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); + + this.Size = sourceSize; + } /// /// Initializes a new instance of the class. @@ -43,10 +49,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) - { - this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - return this; - } + => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -56,10 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) - { - this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - return this; - } + => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// /// Prepends a raw matrix. diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs new file mode 100644 index 000000000..eaa51b129 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public class AffineTransformBuilderTests + { + [Fact] + public void ConstructorAssignsProperties() + { + var s = new Size(1, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + Assert.Equal(s, builder.Size); + } + + [Fact] + public void ConstructorThrowsInvalid() + { + Assert.Throws(() => + { + var s = new Size(0, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); + + Assert.Throws(() => + { + var s = new Size(1, 0); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + var b1 = new AffineTransformBuilder(rectangle); + var b2 = new AffineTransformBuilder(rectangle); + + const float pi = (float)Math.PI; + + // Forwards + b1.AppendRotateMatrixDegrees(pi) + .AppendSkewMatrixDegrees(pi, pi) + .AppendScaleMatrix(new SizeF(pi, pi)) + .AppendTranslationMatrix(new PointF(pi, pi)); + + // Backwards + b2.PrependTranslationMatrix(new PointF(pi, pi)) + .PrependScaleMatrix(new SizeF(pi, pi)) + .PrependSkewMatrixDegrees(pi, pi) + .PrependRotateMatrixDegrees(pi); + + Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); + } + + [Fact] + public void BuilderCanClear() + { + var rectangle = new Rectangle(0, 0, 3, 3); + var builder = new AffineTransformBuilder(rectangle); + Matrix3x2 matrix = Matrix3x2.Identity; + matrix.M31 = (float)Math.PI; + + Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + + builder.AppendMatrix(matrix); + Assert.NotEqual(Matrix3x2.Identity, builder.BuildMatrix()); + + builder.Clear(); + Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs new file mode 100644 index 000000000..3dfc42d4f --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Processing; +using SixLabors.Primitives; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public class ProjectiveTransformBuilderTests + { + [Fact] + public void ConstructorAssignsProperties() + { + var s = new Size(1, 1); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + Assert.Equal(s, builder.Size); + } + + [Fact] + public void ConstructorThrowsInvalid() + { + Assert.Throws(() => + { + var s = new Size(0, 1); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + }); + + Assert.Throws(() => + { + var s = new Size(1, 0); + var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); + }); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + var b1 = new ProjectiveTransformBuilder(rectangle); + var b2 = new ProjectiveTransformBuilder(rectangle); + + const float pi = (float)Math.PI; + + Matrix4x4 m4 = Matrix4x4.Identity; + m4.M31 = pi; + + // Forwards + b1.AppendMatrix(m4) + .AppendTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi); + + // Backwards + b2.PrependTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi) + .PrependMatrix(m4); + + Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); + } + + [Fact] + public void BuilderCanClear() + { + var rectangle = new Rectangle(0, 0, 3, 3); + var builder = new ProjectiveTransformBuilder(rectangle); + Matrix4x4 matrix = Matrix4x4.Identity; + matrix.M31 = (float)Math.PI; + + Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + + builder.AppendMatrix(matrix); + Assert.NotEqual(Matrix4x4.Identity, builder.BuildMatrix()); + + builder.Clear(); + Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + } + } +} From 6a81923ab7f57d7d947f826de1ef426bec5806ca Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Nov 2018 20:34:37 +0000 Subject: [PATCH 22/66] Tighten up coverage on new AotCompilerTools --- tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs b/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs index 5c1dd4bb0..f6397dbd0 100644 --- a/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs +++ b/tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs @@ -10,9 +10,6 @@ namespace SixLabors.ImageSharp.Tests.Advanced public class AotCompilerTests { [Fact] - public void AotCompiler_NoExceptions() - { - AotCompilerTools.Seed(); - } + public void AotCompiler_NoExceptions() => AotCompilerTools.Seed(); } } From 1c258dcae42f959e1fcf0cd355f3c2aad63a5bf6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 13 Nov 2018 09:38:57 +0000 Subject: [PATCH 23/66] Update tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs --- .../Processing/Transforms/AffineTransformTests.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 15b4500bb..3d7fba2c1 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -207,21 +207,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms } } - 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); From 0ac969a711ed043f0586a961b12b4451dc29cbe2 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Thu, 1 Nov 2018 16:25:55 -0700 Subject: [PATCH 24/66] Optimize filling a region with a solid brush when antialias is off --- .../Processors/Drawing/FillRegionProcessor.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs index 1dc63efa2..170e3d34f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs @@ -3,7 +3,7 @@ using System; using System.Buffers; - +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; @@ -168,17 +168,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing { if (!this.Options.Antialias) { + bool hasOnes = false; + bool hasZeros = false; for (int x = 0; x < scanlineWidth; x++) { if (scanline[x] >= 0.5) { scanline[x] = 1; + hasOnes = true; } else { scanline[x] = 0; + hasZeros = true; } } + + if (hasOnes != hasZeros && this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) + { + if (hasOnes) + { + source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrush.Color); + } + + continue; + } } applicator.Apply(scanline, minX, y); @@ -187,5 +201,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing } } } + + private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) + { + solidBrush = this.Brush as SolidBrush; + + if (solidBrush == null) + { + return false; + } + + return this.Options.IsOpaqueColorWithoutBlending(solidBrush.Color); + } } } \ No newline at end of file From 8073404c7773d5808f2372d0c72fe035bc78bb9e Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Thu, 15 Nov 2018 08:55:22 -0800 Subject: [PATCH 25/66] Check for solid brush outside of loops --- .../Processing/Processors/Drawing/FillRegionProcessor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs index 170e3d34f..550c021ca 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs @@ -107,6 +107,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing Span buffer = bBuffer.GetSpan(); Span scanline = bScanline.GetSpan(); + bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush); + for (int y = minY; y < maxY; y++) { if (scanlineDirty) @@ -184,7 +186,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing } } - if (hasOnes != hasZeros && this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) + if (isSolidBrushWithoutBlending && hasOnes != hasZeros) { if (hasOnes) { From f79f1894a342dae48629c5ea756185f7572af313 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 22 Nov 2018 17:09:48 +0000 Subject: [PATCH 26/66] Reduce allocatoins, check bit depth and rename. --- src/ImageSharp/Formats/Png/PngChunkType.cs | 4 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 6 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 107 ++++++++---------- .../Formats/Png/PngChunkTypeTests.cs | 2 +- .../Formats/Png/PngDecoderTests.Chunks.cs | 2 +- 5 files changed, 56 insertions(+), 65 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 7654c1701..1b251a574 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -56,10 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Png Text = 0x74455874U, /// - /// This chunk specifies that the image uses simple transparency: + /// The tRNS chunk specifies that the image uses simple transparency: /// either alpha values associated with palette entries (for indexed-color images) /// or a single transparent color (for grayscale and true color images). /// - PaletteAlpha = 0x74524E53U + Transparency = 0x74524E53U } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index c446184d8..9ffac5e62 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Formats.Png Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length); this.palette = pal; break; - case PngChunkType.PaletteAlpha: + case PngChunkType.Transparency: byte[] alpha = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); this.paletteAlpha = alpha; @@ -306,9 +306,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset) - { - return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); - } + => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); /// /// Attempts to convert a byte array to a new array where each value in the original array is represented by the diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 532e6f747..8a31aaf51 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -292,7 +292,7 @@ namespace SixLabors.ImageSharp.Formats.Png if (pngMetaData.HasTrans) { - this.WriteTransparencyMarkers(stream, pngMetaData); + this.WriteTransparencyChunk(stream, pngMetaData); } this.WritePhysicalChunk(stream, metaData); @@ -305,52 +305,6 @@ namespace SixLabors.ImageSharp.Formats.Png quantized?.Dispose(); } - /// - /// Writes the transparency markers to the stream - /// - /// The containing image data. - /// The image meta data. - private void WriteTransparencyMarkers(Stream stream, PngMetaData pngMetaData) - { - if (pngMetaData.ColorType == PngColorType.Rgb) - { - if (pngMetaData.TransparentRgb48 != null) - { - var r = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.R); - var g = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.R); - var b = BitConverter.GetBytes(pngMetaData.TransparentRgb48.Value.B); - - var alphaArray = r.Concat(g).Concat(b).ToArray(); - - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); - } - else if (pngMetaData.TransparentRgb24 != null) - { - var alphaArray = new byte[6]; - alphaArray[1] = pngMetaData.TransparentRgb24.Value.R; - alphaArray[3] = pngMetaData.TransparentRgb24.Value.G; - alphaArray[5] = pngMetaData.TransparentRgb24.Value.B; - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); - } - } - else if (pngMetaData.ColorType == PngColorType.Grayscale) - { - if (pngMetaData.TransparentGray16 != null) - { - var alphaArray = BitConverter.GetBytes(pngMetaData.TransparentGray16.Value.PackedValue); - - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); - } - else if (pngMetaData.TransparentGray8 != null) - { - var alphaArray = new byte[2]; - alphaArray[1] = pngMetaData.TransparentGray8.Value.PackedValue; - - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaArray, 0, alphaArray.Length); - } - } - } - /// public void Dispose() { @@ -377,7 +331,6 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.pngColorType.Equals(PngColorType.Grayscale)) { - // TODO: Research and add support for grayscale plus tRNS if (this.use16Bit) { // 16 bit grayscale @@ -752,7 +705,7 @@ namespace SixLabors.ImageSharp.Formats.Png // Write the transparency data if (anyAlpha) { - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, paletteLength); + this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); } } } @@ -800,6 +753,52 @@ namespace SixLabors.ImageSharp.Formats.Png } } + /// + /// Writes the transparency chunk to the stream + /// + /// The containing image data. + /// The image meta data. + private void WriteTransparencyChunk(Stream stream, PngMetaData pngMetaData) + { + if (pngMetaData.ColorType.Equals(PngColorType.Rgb)) + { + Span alpha = this.buffer.AsSpan(); + if (pngMetaData.TransparentRgb48.HasValue && this.use16Bit) + { + Rgb48 rgb = pngMetaData.TransparentRgb48.Value; + BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); + + this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 6); + } + else if (pngMetaData.TransparentRgb24.HasValue) + { + alpha.Clear(); + Rgb24 rgb = pngMetaData.TransparentRgb24.Value; + alpha[1] = rgb.R; + alpha[3] = rgb.G; + alpha[5] = rgb.B; + this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 6); + } + } + else if (pngMetaData.ColorType.Equals(PngColorType.Grayscale)) + { + Span alpha = this.buffer.AsSpan(); + if (pngMetaData.TransparentGray16.HasValue && this.bitDepth == 16) + { + BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetaData.TransparentGray16.Value.PackedValue); + this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 2); + } + else if (pngMetaData.TransparentGray8.HasValue) + { + alpha.Clear(); + alpha[1] = pngMetaData.TransparentGray8.Value.PackedValue; + this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 2); + } + } + } + /// /// Writes the pixel information to the stream. /// @@ -894,10 +893,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Writes the chunk end to the stream. /// /// The containing image data. - private void WriteEndChunk(Stream stream) - { - this.WriteChunk(stream, PngChunkType.End, null); - } + private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null); /// /// Writes a chunk to the stream. @@ -905,10 +901,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The to write to. /// The type of chunk to write. /// The containing data. - private void WriteChunk(Stream stream, PngChunkType type, byte[] data) - { - this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); - } + private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); /// /// Writes a chunk of a specified length to the stream at the given offset. diff --git a/tests/ImageSharp.Tests/Formats/Png/PngChunkTypeTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngChunkTypeTests.cs index e4cd06ab1..894d902b7 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngChunkTypeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngChunkTypeTests.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png Assert.Equal(PngChunkType.Palette, GetType("PLTE")); Assert.Equal(PngChunkType.Data, GetType("IDAT")); Assert.Equal(PngChunkType.End, GetType("IEND")); - Assert.Equal(PngChunkType.PaletteAlpha, GetType("tRNS")); + Assert.Equal(PngChunkType.Transparency, GetType("tRNS")); Assert.Equal(PngChunkType.Text, GetType("tEXt")); Assert.Equal(PngChunkType.Gamma, GetType("gAMA")); Assert.Equal(PngChunkType.Physical, GetType("pHYs")); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 2a7d69616..6a0119f0f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [Theory] [InlineData((uint)PngChunkType.Gamma)] // gAMA - [InlineData((uint)PngChunkType.PaletteAlpha)] // tRNS + [InlineData((uint)PngChunkType.Transparency)] // tRNS [InlineData((uint)PngChunkType.Physical)] // pHYs: It's ok to test physical as we don't throw for duplicate chunks. //[InlineData(PngChunkTypes.Text)] //TODO: Figure out how to test this public void Decode_IncorrectCRCForNonCriticalChunk_ExceptionIsThrown(uint chunkType) From ead17fd0179c98b99b3212b9213bcefbf9335e89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 22 Nov 2018 17:12:43 +0000 Subject: [PATCH 27/66] Use existing field --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8a31aaf51..bb800c8a4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -785,7 +785,7 @@ namespace SixLabors.ImageSharp.Formats.Png else if (pngMetaData.ColorType.Equals(PngColorType.Grayscale)) { Span alpha = this.buffer.AsSpan(); - if (pngMetaData.TransparentGray16.HasValue && this.bitDepth == 16) + if (pngMetaData.TransparentGray16.HasValue && this.use16Bit) { BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetaData.TransparentGray16.Value.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 2); From 68496170654216571d54261d3f7074b8e9194a45 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 23 Nov 2018 12:25:05 +0000 Subject: [PATCH 28/66] Fix trns preservation and add tests --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 11 ++- src/ImageSharp/Formats/Png/PngMetaData.cs | 5 ++ .../Formats/Png/PngEncoderTests.cs | 67 ++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 3 + tests/Images/Input/Png/gray-2-tRNS.png | Bin 0 -> 265 bytes tests/Images/Input/Png/gray-4-tRNS.png | Bin 0 -> 267 bytes tests/Images/Input/Png/gray-8-tRNS.png | Bin 0 -> 267 bytes 7 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 tests/Images/Input/Png/gray-2-tRNS.png create mode 100644 tests/Images/Input/Png/gray-4-tRNS.png create mode 100644 tests/Images/Input/Png/gray-8-tRNS.png diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index bb800c8a4..292de007c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -760,9 +760,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The image meta data. private void WriteTransparencyChunk(Stream stream, PngMetaData pngMetaData) { + Span alpha = this.chunkDataBuffer.AsSpan(); if (pngMetaData.ColorType.Equals(PngColorType.Rgb)) { - Span alpha = this.buffer.AsSpan(); if (pngMetaData.TransparentRgb48.HasValue && this.use16Bit) { Rgb48 rgb = pngMetaData.TransparentRgb48.Value; @@ -770,7 +770,7 @@ namespace SixLabors.ImageSharp.Formats.Png BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); - this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); } else if (pngMetaData.TransparentRgb24.HasValue) { @@ -779,22 +779,21 @@ namespace SixLabors.ImageSharp.Formats.Png alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; - this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); } } else if (pngMetaData.ColorType.Equals(PngColorType.Grayscale)) { - Span alpha = this.buffer.AsSpan(); if (pngMetaData.TransparentGray16.HasValue && this.use16Bit) { BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetaData.TransparentGray16.Value.PackedValue); - this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); } else if (pngMetaData.TransparentGray8.HasValue) { alpha.Clear(); alpha[1] = pngMetaData.TransparentGray8.Value.PackedValue; - this.WriteChunk(stream, PngChunkType.Transparency, this.buffer, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); } } } diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 6a293f770..d5ab3d255 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Png this.BitDepth = other.BitDepth; this.ColorType = other.ColorType; this.Gamma = other.Gamma; + this.HasTrans = other.HasTrans; + this.TransparentGray8 = other.TransparentGray8; + this.TransparentGray16 = other.TransparentGray16; + this.TransparentRgb24 = other.TransparentRgb24; + this.TransparentRgb48 = other.TransparentRgb48; } /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 5d328db36..9079b15fb 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -25,6 +25,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { TestImages.Png.Bpp1, PngBitDepth.Bit1 } }; + public static readonly TheoryData PngTrnsFiles = + new TheoryData + { + { TestImages.Png.Gray1BitTrans, PngBitDepth.Bit1, PngColorType.Grayscale }, + { TestImages.Png.Gray2BitTrans, PngBitDepth.Bit2, PngColorType.Grayscale }, + { TestImages.Png.Gray4BitTrans, PngBitDepth.Bit4, PngColorType.Grayscale }, + { TestImages.Png.Gray8BitTrans, PngBitDepth.Bit8, PngColorType.Grayscale }, + { TestImages.Png.GrayTrns16BitInterlaced, PngBitDepth.Bit16, PngColorType.Grayscale }, + { TestImages.Png.Rgb24BppTrans, PngBitDepth.Bit8, PngColorType.Rgb }, + { TestImages.Png.Rgb48BppTrans, PngBitDepth.Bit16, PngColorType.Rgb } + }; + /// /// All types except Palette /// @@ -249,6 +261,61 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [Theory] + [MemberData(nameof(PngTrnsFiles))] + public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) + { + var options = new PngEncoder(); + + var testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateImage()) + { + PngMetaData inMeta = input.MetaData.GetFormatMetaData(PngFormat.Instance); + Assert.True(inMeta.HasTrans); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + PngMetaData outMeta = output.MetaData.GetFormatMetaData(PngFormat.Instance); + Assert.True(outMeta.HasTrans); + + switch (pngColorType) + { + case PngColorType.Grayscale: + if (pngBitDepth.Equals(PngBitDepth.Bit16)) + { + Assert.True(outMeta.TransparentGray16.HasValue); + Assert.Equal(inMeta.TransparentGray16, outMeta.TransparentGray16); + } + else + { + Assert.True(outMeta.TransparentGray8.HasValue); + Assert.Equal(inMeta.TransparentGray8, outMeta.TransparentGray8); + } + + break; + case PngColorType.Rgb: + if (pngBitDepth.Equals(PngBitDepth.Bit16)) + { + Assert.True(outMeta.TransparentRgb48.HasValue); + Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48); + } + else + { + Assert.True(outMeta.TransparentRgb24.HasValue); + Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24); + } + + break; + } + } + } + } + } + private static void TestPngEncoderCore( TestImageProvider provider, PngColorType pngColorType, diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8da458e52..1144a3f7c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -46,6 +46,9 @@ namespace SixLabors.ImageSharp.Tests public const string PDSrc = "Png/pd-source.png"; public const string PDDest = "Png/pd-dest.png"; public const string Gray1BitTrans = "Png/gray-1-trns.png"; + public const string Gray2BitTrans = "Png/gray-2-tRNS.png"; + public const string Gray4BitTrans = "Png/gray-4-tRNS.png"; + public const string Gray8BitTrans = "Png/gray-8-tRNS.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/gray-2-tRNS.png b/tests/Images/Input/Png/gray-2-tRNS.png new file mode 100644 index 0000000000000000000000000000000000000000..8e04cb5020cc1ecf62544cb32d5b79c80db9eb55 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c21SA-yJ@?oEq?k&A{DK)68K?YR2juCLxJHyX z=ND8KWu|A8FsxKCGB7mO0TQMPUile$3eKf@d6{|X8Hu?HPWk0IsYSIV#s7f%N-{$t zN_;YtQ}c>}(hQ6Ysd)?x9M^SweEFIc1X|aBdc-KRJ9m18V9RtbC7#BO-2Ur1W9#j; zlez9mBy!#Etjcre>X=olwIZjW_ui%cf?1oV^|fysu_zijiZ`$`Yrw=Fz( zYoAeHvWEEVQdfX$D4y)I0_Tj_bNTzI@FJ0MksZvXY1vGw-a z$z1m&61i@7R^_>KbOT9H%Gd+$_Bo*Zy%#K^eU$*(yeWi!*+ZLX? zwa=(8SwnpGsmmvLjg7RLZp>J#2DG2Y)5S4_<9c#R2+&W=ml!^t%;`7}6ld^s^>bP0 Hl+XkKeMDTy literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Png/gray-8-tRNS.png b/tests/Images/Input/Png/gray-8-tRNS.png new file mode 100644 index 0000000000000000000000000000000000000000..842245f1d9f192857edd85f43f2d8d6ca8874c01 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K57&$`wkAjMP?KCsyuhDj#;%@D{=~Y?_KIIn6-IYU;E~gq@uio*>TJG%QoM-uk`SJ+ro3V z_8IjhYlzQ2b@>FZv5{8OjTvjzfcEovx;TbNTux4rdC0)f)WpbSSo>uLP@KWj)z4*} HQ$iB}V~<+M literal 0 HcmV?d00001 From da50180e1c7a2aa34d94d578824deec4ea3e6b0a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 22:52:47 +0100 Subject: [PATCH 29/66] add more tests --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 15 +- .../Processing/AffineTransformBuilder.cs | 35 ++-- .../Processors/Transforms/TransformUtils.cs | 11 ++ .../Transforms/AffineTransformBuilderTests.cs | 67 +++----- .../Transforms/TransformBuilderTestBase.cs | 151 ++++++++++++++++++ .../TestUtilities/ApproximateFloatComparer.cs | 13 +- 6 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index f07ccb03b..45556741e 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -22,7 +22,8 @@ namespace SixLabors.ImageSharp /// The blue component. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f); + public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => + (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f); /// /// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709. @@ -32,7 +33,8 @@ namespace SixLabors.ImageSharp /// The blue component. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F)); + public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => + (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F)); /// /// Scales a value from a 16 bit to it's 8 bit equivalent. @@ -128,6 +130,15 @@ namespace SixLabors.ImageSharp return x & (m - 1); } + /// + /// Converts degrees to radians + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static float ToRadian(float degrees) + { + return degrees * ((float)Math.PI / 180f); + } + /// /// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation. /// diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 003249d6e..4d798f439 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -14,18 +14,15 @@ namespace SixLabors.ImageSharp.Processing public class AffineTransformBuilder { private readonly List matrices = new List(); - private Rectangle rectangle; + private readonly Rectangle rectangle; /// /// Initializes a new instance of the class. /// /// The source image size. public AffineTransformBuilder(Size sourceSize) + : this(new Rectangle(Point.Empty, sourceSize)) { - Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); - Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); - - this.Size = sourceSize; } /// @@ -33,13 +30,17 @@ namespace SixLabors.ImageSharp.Processing /// /// The source rectangle. public AffineTransformBuilder(Rectangle sourceRectangle) - : this(sourceRectangle.Size) - => this.rectangle = sourceRectangle; + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + this.rectangle = sourceRectangle; + } /// /// Gets the source image size. /// - internal Size Size { get; } + internal Size Size => this.rectangle.Size; /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -49,13 +50,29 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + /// + /// Prepends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder PrependRotateMatrixRadians(float radians) + => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + /// /// Appends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + => this.AppendRotateMatrixRadians(ImageMaths.ToRadian(degrees)); + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder AppendRotateMatrixRadians(float radians) + => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// /// Prepends a scale matrix from the given vector scale. diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index f561d3513..6cef38f8f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -23,6 +23,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + /// + /// Creates a centered rotation matrix using the given rotation in radians and the source size. + /// + /// The amount of rotation, in radians. + /// The source image size. + /// The . + public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); + /// /// Creates a centered skew matrix from the give angles in degrees and the source size. /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index eaa51b129..1b4caee14 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -4,12 +4,13 @@ using System; using System.Numerics; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - public class AffineTransformBuilderTests + public class AffineTransformBuilderTests : TransformBuilderTestBase { [Fact] public void ConstructorAssignsProperties() @@ -23,57 +24,33 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void ConstructorThrowsInvalid() { Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); + { + var s = new Size(0, 1); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); + { + var s = new Size(1, 0); + var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); + }); } - [Fact] - public void AppendPrependOpposite() - { - var rectangle = new Rectangle(-1, -1, 3, 3); - var b1 = new AffineTransformBuilder(rectangle); - var b2 = new AffineTransformBuilder(rectangle); - - const float pi = (float)Math.PI; - - // Forwards - b1.AppendRotateMatrixDegrees(pi) - .AppendSkewMatrixDegrees(pi, pi) - .AppendScaleMatrix(new SizeF(pi, pi)) - .AppendTranslationMatrix(new PointF(pi, pi)); - - // Backwards - b2.PrependTranslationMatrix(new PointF(pi, pi)) - .PrependScaleMatrix(new SizeF(pi, pi)) - .PrependSkewMatrixDegrees(pi, pi) - .PrependRotateMatrixDegrees(pi); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslationMatrix(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScaleMatrix(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotateMatrixRadians(radians); - Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); - } + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslationMatrix(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScaleMatrix(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotateMatrixRadians(radians); - [Fact] - public void BuilderCanClear() + protected override Vector2 Execute( + AffineTransformBuilder builder, + Rectangle rectangle, + Vector2 sourcePoint) { - var rectangle = new Rectangle(0, 0, 3, 3); - var builder = new AffineTransformBuilder(rectangle); - Matrix3x2 matrix = Matrix3x2.Identity; - matrix.M31 = (float)Math.PI; - - Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); - - builder.AppendMatrix(matrix); - Assert.NotEqual(Matrix3x2.Identity, builder.BuildMatrix()); - - builder.Clear(); - Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix()); + Matrix3x2 matrix = builder.BuildMatrix(); + return Vector2.Transform(sourcePoint, matrix); } } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs new file mode 100644 index 000000000..d109387cc --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.Primitives; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + public abstract class TransformBuilderTestBase + { + private static readonly ApproximateFloatComparer Comparer = new ApproximateFloatComparer(1e-6f); + + public static readonly TheoryData ScaleTranslate_Data = + new TheoryData + { + // scale, translate, source, expectedDest + + { Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero }, + { Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) }, + { Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) }, + { new Vector2(2, 0.5f), new Vector2(3, 1), new Vector2(10, 20), new Vector2(23, 11) }, + }; + + [Theory] + [MemberData(nameof(ScaleTranslate_Data))] + public void _1Scale_2Translate(Vector2 scale, Vector2 translate, Vector2 source, Vector2 expectedDest) + { + // These operations should be size-agnostic: + var size = new Size(123, 321); + TBuilder builder = this.CreateBuilder(size); + + this.AppendScale(builder, new SizeF(scale)); + this.AppendTranslation(builder, translate); + + Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); + Assert.True(Comparer.Equals(expectedDest, actualDest)); + } + + public static readonly TheoryData TranslateScale_Data = + new TheoryData + { + // translate, scale, source, expectedDest + + { Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero }, + { Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) }, + { new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) }, + }; + + [Theory] + [MemberData(nameof(TranslateScale_Data))] + public void _1Translate_2Scale(Vector2 translate, Vector2 scale, Vector2 source, Vector2 expectedDest) + { + // Translate ans scale are size-agnostic: + var size = new Size(456, 432); + TBuilder builder = this.CreateBuilder(size); + + this.AppendTranslation(builder, translate); + this.AppendScale(builder, new SizeF(scale)); + + Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source); + Assert.Equal(expectedDest, actualDest, Comparer); + } + + [Theory] + [InlineData(10, 20)] + [InlineData(-20, 10)] + public void LocationOffsetIsPrepended(int locationX, int locationY) + { + var rectangle = new Rectangle(locationX, locationY, 10, 10); + TBuilder builder = this.CreateBuilder(rectangle); + + this.AppendScale(builder, new SizeF(2, 2)); + + Vector2 actual = this.Execute(builder, rectangle, Vector2.One); + Vector2 expected = new Vector2(-locationX + 1, -locationY + 1) * 2; + + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 42, 84)] + [InlineData(200, 100, 100, 42, 84)] + [InlineData(100, 200, -10, 42, 84)] + public void RotateDegrees_ShouldCreateCenteredMatrix(int width, int height, float deg, float x, float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + this.AppendRotationDegrees(builder, deg); + + // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness + Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + + [Fact] + public void AppendPrependOpposite() + { + var rectangle = new Rectangle(-1, -1, 3, 3); + TBuilder b1 = this.CreateBuilder(rectangle); + TBuilder b2 = this.CreateBuilder(rectangle); + + const float pi = (float)Math.PI; + + // Forwards + this.AppendRotationRadians(b1, pi); + this.AppendScale(b1, new SizeF(2, 0.5f)); + this.AppendTranslation(b1, new PointF(123, 321)); + + // Backwards + this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependScale(b2, new SizeF(2, 0.5f)); + this.PrependRotationRadians(b2, pi); + + Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65)); + Vector2 p2 = this.Execute(b2, rectangle, new Vector2(32, 65)); + + Assert.Equal(p1, p2, Comparer); + } + + protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size)); + + protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle); + + protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationRadians(TBuilder builder, float radians); + + protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void PrependRotationRadians(TBuilder builder, float radians); + + protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => + this.AppendRotationRadians(builder, ImageMaths.ToRadian(degrees)); + + protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); + + private static float Sqrt(float a) => (float)Math.Sqrt(a); + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 854e57d8f..47ca6cccb 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Tests /// internal readonly struct ApproximateFloatComparer : IEqualityComparer, - IEqualityComparer + IEqualityComparer, + IEqualityComparer { private readonly float Epsilon; @@ -33,9 +34,17 @@ namespace SixLabors.ImageSharp.Tests public int GetHashCode(float obj) => obj.GetHashCode(); /// - public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W); + public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W); /// public int GetHashCode(Vector4 obj) => obj.GetHashCode(); + + /// + public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y); + + public int GetHashCode(Vector2 obj) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file From cc2589f88e7887127b19303a19ad5ae0974cb013 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:07:50 +0100 Subject: [PATCH 30/66] rename methods + extend API --- .../Processing/AffineTransformBuilder.cs | 78 +++++++++++++++---- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 6 +- .../Transforms/AffineTransformBuilderTests.cs | 12 +-- .../Transforms/AffineTransformTests.cs | 20 ++--- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 4d798f439..1620b7223 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees) + public AffineTransformBuilder PrependRotationDegrees(float degrees) => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); /// @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotateMatrixRadians(float radians) + public AffineTransformBuilder PrependRotationRadians(float radians) => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// @@ -63,32 +63,64 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees) - => this.AppendRotateMatrixRadians(ImageMaths.ToRadian(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.ToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendRotateMatrixRadians(float radians) + public AffineTransformBuilder AppendRotationRadians(float radians) => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix3x2.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public AffineTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix3x2.CreateScale(scale)); + /// /// Prepends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder PrependScaleMatrix(SizeF scales) - => this.PrependMatrix(Matrix3x2Extensions.CreateScale(scales)); + public AffineTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix3x2.CreateScale(scales)); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScaleMatrix(SizeF scales) - => this.AppendMatrix(Matrix3x2Extensions.CreateScale(scales)); + public AffineTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix3x2.CreateScale(scales)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public AffineTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -96,7 +128,7 @@ namespace SixLabors.ImageSharp.Processing /// The X angle, in degrees. /// The Y angle, in degrees. /// The . - public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY) + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// @@ -105,7 +137,7 @@ namespace SixLabors.ImageSharp.Processing /// The X angle, in degrees. /// The Y angle, in degrees. /// The . - public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY) + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); /// @@ -113,16 +145,32 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslationMatrix(PointF position) - => this.PrependMatrix(Matrix3x2Extensions.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslationMatrix(PointF position) - => this.AppendMatrix(Matrix3x2Extensions.CreateTranslation(position)); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a raw matrix. diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 564318e5e..a29342ad4 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,9 +74,9 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) - .AppendRotateMatrixDegrees(45F) - .AppendScaleMatrix(new SizeF(.25F, .25F)) - .AppendTranslationMatrix(new PointF(10, 10)); + .AppendRotationDegrees(45F) + .AppendScale(new SizeF(.25F, .25F)) + .AppendTranslation(new PointF(10, 10)); // Apply a background color so we can see the translation. blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors.HotPink)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 1b4caee14..639f4b46f 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -36,13 +36,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }); } - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslationMatrix(translate); - protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScaleMatrix(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotateMatrixRadians(radians); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslationMatrix(translate); - protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScaleMatrix(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotateMatrixRadians(radians); + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 3d7fba2c1..e78bb2c6d 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(30); + .AppendRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,9 +102,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(angleDeg) - .AppendScaleMatrix(new SizeF(sx, sy)) - .AppendTranslationMatrix(new PointF(tx, ty)); + .AppendRotationDegrees(angleDeg) + .AppendScale(new SizeF(sx, sy)) + .AppendTranslation(new PointF(tx, ty)); this.PrintMatrix(builder.BuildMatrix()); @@ -124,8 +124,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(angleDeg) - .AppendScaleMatrix(new SizeF(s, s)); + .AppendRotationDegrees(angleDeg) + .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -160,7 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) - .AppendScaleMatrix(new SizeF(2, 1.5F)); + .AppendScale(new SizeF(2, 1.5F)); image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) - .AppendScaleMatrix(new SizeF(1F, 2F)); + .AppendScale(new SizeF(1F, 2F)); image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); @@ -197,8 +197,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotateMatrixDegrees(50) - .AppendScaleMatrix(new SizeF(.6F, .6F)); + .AppendRotationDegrees(50) + .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); From 0774d5e794d41eb342b05af7ceb65df1c8258681 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:18:17 +0100 Subject: [PATCH 31/66] Translation, Scale -> ProjectiveTransformBuilder --- .../Processing/AffineTransformBuilder.cs | 26 ++-- .../Processing/ProjectiveTransformBuilder.cs | 113 ++++++++++++++++-- .../ProjectiveTransformBuilderTests.cs | 54 +++------ 3 files changed, 131 insertions(+), 62 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 1620b7223..cd66272b4 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing public class AffineTransformBuilder { private readonly List matrices = new List(); - private readonly Rectangle rectangle; + private readonly Rectangle sourceRectangle; /// /// Initializes a new instance of the class. @@ -34,13 +34,13 @@ namespace SixLabors.ImageSharp.Processing Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - this.rectangle = sourceRectangle; + this.sourceRectangle = sourceRectangle; } /// /// Gets the source image size. /// - internal Size Size => this.rectangle.Size; + internal Size Size => this.sourceRectangle.Size; /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -145,32 +145,32 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(Vector2 position) - => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Prepends a raw matrix. @@ -203,9 +203,9 @@ namespace SixLabors.ImageSharp.Processing Matrix3x2 matrix = Matrix3x2.Identity; // Translate the origin matrix to cater for source rectangle offsets. - if (!this.rectangle.Equals(default)) + if (!this.sourceRectangle.Equals(default)) { - matrix *= Matrix3x2.CreateTranslation(-this.rectangle.Location); + matrix *= Matrix3x2.CreateTranslation(-this.sourceRectangle.Location); } foreach (Matrix3x2 m in this.matrices) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index be4c67518..6c5fb4625 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -14,18 +14,15 @@ namespace SixLabors.ImageSharp.Processing public class ProjectiveTransformBuilder { private readonly List matrices = new List(); - private Rectangle rectangle; + private Rectangle sourceRectangle; /// /// Initializes a new instance of the class. /// /// The source image size. public ProjectiveTransformBuilder(Size sourceSize) + : this(new Rectangle(Point.Empty, sourceSize)) { - Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize)); - Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize)); - - this.Size = sourceSize; } /// @@ -33,13 +30,18 @@ namespace SixLabors.ImageSharp.Processing /// /// The source rectangle. public ProjectiveTransformBuilder(Rectangle sourceRectangle) - : this(sourceRectangle.Size) - => this.rectangle = sourceRectangle; + { + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); + + this.sourceRectangle = sourceRectangle; + } /// /// Gets the source image size. /// - internal Size Size { get; } + internal Size Size => this.sourceRectangle.Size; + /// /// Prepends a matrix that performs a tapering projective transform. @@ -61,6 +63,96 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + public void AppendRotationRadians(float radians) + { + throw new System.NotImplementedException(); + } + + public void PrependRotationRadians(float radians) + { + throw new System.NotImplementedException(); + } + + /// + /// Prepends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Appends a scale matrix from the given uniform scale. + /// + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix4x4.CreateScale(scale)); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + + /// + /// Prepends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); + + /// + /// Appends a scale matrix from the given vector scale. + /// + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + + /// + /// Prepends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); + + /// + /// Appends a translation matrix from the given vector. + /// + /// The translation position. + /// The . + public ProjectiveTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); + /// /// Prepends a raw matrix. /// @@ -92,9 +184,9 @@ namespace SixLabors.ImageSharp.Processing Matrix4x4 matrix = Matrix4x4.Identity; // Translate the origin matrix to cater for source rectangle offsets. - if (!this.rectangle.Equals(default)) + if (!this.sourceRectangle.Equals(default)) { - matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.rectangle.Location, 0)); + matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.sourceRectangle.Location, 0)); } foreach (Matrix4x4 m in this.matrices) @@ -109,5 +201,6 @@ namespace SixLabors.ImageSharp.Processing /// Removes all matrices from the builder. /// public void Clear() => this.matrices.Clear(); + } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 3dfc42d4f..0d9bd301d 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -9,7 +9,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { - public class ProjectiveTransformBuilderTests + public class ProjectiveTransformBuilderTests : TransformBuilderTestBase { [Fact] public void ConstructorAssignsProperties() @@ -34,45 +34,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); }); } - - [Fact] - public void AppendPrependOpposite() - { - var rectangle = new Rectangle(-1, -1, 3, 3); - var b1 = new ProjectiveTransformBuilder(rectangle); - var b2 = new ProjectiveTransformBuilder(rectangle); - - const float pi = (float)Math.PI; - - Matrix4x4 m4 = Matrix4x4.Identity; - m4.M31 = pi; - - // Forwards - b1.AppendMatrix(m4) - .AppendTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi); - - // Backwards - b2.PrependTaperMatrix(TaperSide.Left, TaperCorner.LeftOrTop, pi) - .PrependMatrix(m4); - - Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix()); - } - - [Fact] - public void BuilderCanClear() + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + + protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + + protected override Vector2 Execute( + ProjectiveTransformBuilder builder, + Rectangle rectangle, + Vector2 sourcePoint) { - var rectangle = new Rectangle(0, 0, 3, 3); - var builder = new ProjectiveTransformBuilder(rectangle); - Matrix4x4 matrix = Matrix4x4.Identity; - matrix.M31 = (float)Math.PI; - - Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); - - builder.AppendMatrix(matrix); - Assert.NotEqual(Matrix4x4.Identity, builder.BuildMatrix()); - - builder.Clear(); - Assert.Equal(Matrix4x4.Identity, builder.BuildMatrix()); + Matrix4x4 matrix = builder.BuildMatrix(); + return Vector2.Transform(sourcePoint, matrix); } } } From e5f7b12c7ee165b2996cfe3d704c9ce6ac2e6e2d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:29:34 +0100 Subject: [PATCH 32/66] extend ProjectiveTransformBuilder --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 2 +- .../Processing/AffineTransformBuilder.cs | 29 +++++++------- .../Processing/ProjectiveTransformBuilder.cs | 38 ++++++++++++++++--- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 4 +- .../Transforms/AffineTransformTests.cs | 8 ++-- .../ProjectiveTransformBuilderTests.cs | 5 ++- .../Transforms/TransformBuilderTestBase.cs | 2 +- 8 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 45556741e..c0c8e07e6 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp /// Converts degrees to radians /// [MethodImpl(InliningOptions.ShortMethod)] - public static float ToRadian(float degrees) + public static float DegreesToRadians(float degrees) { return degrees * ((float)Math.PI / 180f); } diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index cd66272b4..1f26d079d 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,37 +42,38 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; + /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a centered rotation matrix using the given rotation in radians. /// - /// The amount of rotation, in degrees. + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size)); + public AffineTransformBuilder PrependCenteredRotationRadians(float radians) + => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// - /// Prepends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder PrependRotationRadians(float radians) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + public AffineTransformBuilder AppendCenteredRotationRadians(float radians) + => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Prepends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.ToRadian(degrees)); + public AffineTransformBuilder PrependCenteredRotationDegrees(float degrees) + => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in degrees. /// - /// The amount of rotation, in radians. + /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + public AffineTransformBuilder AppendCenteredRotationDegrees(float degrees) + => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 6c5fb4625..58c02108a 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); this.sourceRectangle = sourceRectangle; - } + } /// /// Gets the source image size. @@ -63,16 +63,44 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); - public void AppendRotationRadians(float radians) + /// + /// Prepends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder PrependCenteredRotationRadians(float radians) { - throw new System.NotImplementedException(); + var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + return this.PrependMatrix(m); } - public void PrependRotationRadians(float radians) + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The . + public ProjectiveTransformBuilder AppendCenteredRotationRadians(float radians) { - throw new System.NotImplementedException(); + var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + return this.AppendMatrix(m); } + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) + => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder AppendCenteredRotationDegrees(float degrees) + => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + /// /// Prepends a scale matrix from the given uniform scale. /// diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index a29342ad4..2f8923654 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) - .AppendRotationDegrees(45F) + .AppendCenteredRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 639f4b46f..2a43e56e7 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -38,11 +38,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index e78bb2c6d..f86db5641 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(30); + .AppendCenteredRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(angleDeg) + .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); @@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(angleDeg) + .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) - .AppendRotationDegrees(50) + .AppendCenteredRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 0d9bd301d..f467bed40 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); }); } + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); protected override Vector2 Execute( ProjectiveTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index d109387cc..3f259c9aa 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder builder, float radians); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, ImageMaths.ToRadian(degrees)); + this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); From 59f47d457b1c72d8ae2baa49fdf351932f7bb9fd Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 25 Nov 2018 23:31:05 +0100 Subject: [PATCH 33/66] fix trailing whitespace warnings --- src/ImageSharp/Processing/AffineTransformBuilder.cs | 1 - src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 1f26d079d..a1305d121 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -42,7 +42,6 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; - /// /// Prepends a centered rotation matrix using the given rotation in radians. /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 58c02108a..9bcbaf834 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -42,7 +42,6 @@ namespace SixLabors.ImageSharp.Processing /// internal Size Size => this.sourceRectangle.Size; - /// /// Prepends a matrix that performs a tapering projective transform. /// @@ -229,6 +228,5 @@ namespace SixLabors.ImageSharp.Processing /// Removes all matrices from the builder. /// public void Clear() => this.matrices.Clear(); - } } \ No newline at end of file From 103d12901234874c91131faeedc1123088830581 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:09:47 +0100 Subject: [PATCH 34/66] Make AffineTransformProcessor and AffineTransformBuilder API more flexible --- .../Processing/AffineTransformBuilder.cs | 83 ++++++++----------- .../Transforms/AffineTransformProcessor.cs | 6 +- .../Processing/TransformExtensions.cs | 71 ++++++++++++++-- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 28 +------ .../Transforms/AffineTransformTests.cs | 18 ++-- .../ProjectiveTransformBuilderTests.cs | 2 + .../Transforms/TransformBuilderTestBase.cs | 18 +++- 8 files changed, 136 insertions(+), 92 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index a1305d121..42d555f20 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -13,34 +14,7 @@ namespace SixLabors.ImageSharp.Processing /// public class AffineTransformBuilder { - private readonly List matrices = new List(); - private readonly Rectangle sourceRectangle; - - /// - /// Initializes a new instance of the class. - /// - /// The source image size. - public AffineTransformBuilder(Size sourceSize) - : this(new Rectangle(Point.Empty, sourceSize)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The source rectangle. - public AffineTransformBuilder(Rectangle sourceRectangle) - { - Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); - Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - - this.sourceRectangle = sourceRectangle; - } - - /// - /// Gets the source image size. - /// - internal Size Size => this.sourceRectangle.Size; + private readonly List> matrixFactories = new List>(); /// /// Prepends a centered rotation matrix using the given rotation in radians. @@ -48,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public AffineTransformBuilder PrependCenteredRotationRadians(float radians) - => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// /// Appends a centered rotation matrix using the given rotation in radians. @@ -56,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendCenteredRotationRadians(float radians) - => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); + => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -129,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + => this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); /// /// Appends a centered skew matrix from the give angles in degrees. @@ -138,7 +112,7 @@ namespace SixLabors.ImageSharp.Processing /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size)); + => this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); /// /// Prepends a translation matrix from the given vector. @@ -179,7 +153,7 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) { - this.matrices.Insert(0, matrix); + this.matrixFactories.Insert(0, _ => matrix); return this; } @@ -190,35 +164,50 @@ namespace SixLabors.ImageSharp.Processing /// The . public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) { - this.matrices.Add(matrix); + this.matrixFactories.Add(_ => matrix); return this; } /// - /// Returns the combined matrix. + /// Returns the combined matrix for a given source size. + /// + /// The source image size + /// The . + public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); + + /// + /// Returns the combined matrix for a given source rectangle. /// + /// The rectangle in the source image /// The . - public Matrix3x2 BuildMatrix() + public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) { - Matrix3x2 matrix = Matrix3x2.Identity; + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); // Translate the origin matrix to cater for source rectangle offsets. - if (!this.sourceRectangle.Equals(default)) - { - matrix *= Matrix3x2.CreateTranslation(-this.sourceRectangle.Location); - } + var matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - foreach (Matrix3x2 m in this.matrices) + Size size = sourceRectangle.Size; + + foreach (Func factory in this.matrixFactories) { - matrix *= m; + matrix *= factory(size); } return matrix; } - /// - /// Removes all matrices from the builder. - /// - public void Clear() => this.matrices.Clear(); + private AffineTransformBuilder Prepend(Func factory) + { + this.matrixFactories.Insert(0, factory); + return this; + } + + private AffineTransformBuilder Append(Func factory) + { + this.matrixFactories.Add(factory); + return this; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 2370adb86..f357b6c27 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -24,13 +24,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size. - public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize) + /// The target dimensions. + public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) { Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); + this.TargetDimensions = targetDimensions;; } /// diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index ea789eb3d..de4a3e5ba 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -1,8 +1,11 @@ // 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.Transforms; +using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { @@ -11,6 +14,29 @@ namespace SixLabors.ImageSharp.Processing /// public static class TransformExtensions { + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + Matrix3x2 transform, + Size targetDimensions, + IResampler sampler) + where TPixel : struct, IPixel + { + return ctx.ApplyProcessor( + new AffineTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + } + /// /// Performs an affine transform of an image. /// @@ -18,7 +44,9 @@ namespace SixLabors.ImageSharp.Processing /// The image to transform. /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + AffineTransformBuilder builder) where TPixel : struct, IPixel => Transform(source, builder, KnownResamplers.Bicubic); @@ -26,13 +54,39 @@ namespace SixLabors.ImageSharp.Processing /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . + /// The source rectangle + /// The affine transform builder. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + AffineTransformBuilder builder, + IResampler sampler) + where TPixel : struct, IPixel + { + Matrix3x2 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform); + return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs an affine transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . /// The affine transform builder. /// The to perform the resampling. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, AffineTransformBuilder builder, IResampler sampler) + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + AffineTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new AffineTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); + { + return ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + } /// /// Performs a projective transform of an image. @@ -41,7 +95,9 @@ namespace SixLabors.ImageSharp.Processing /// The image to transform. /// The affine transform builder. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder) where TPixel : struct, IPixel => Transform(source, builder, KnownResamplers.Bicubic); @@ -53,7 +109,10 @@ namespace SixLabors.ImageSharp.Processing /// The projective transform builder. /// The to perform the resampling. /// The - public static IImageProcessingContext Transform(this IImageProcessingContext source, ProjectiveTransformBuilder builder, IResampler sampler) + public static IImageProcessingContext Transform( + this IImageProcessingContext source, + ProjectiveTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); } diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index 2f8923654..f3772e3aa 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests using (Image image = provider.GetImage()) using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { - AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 2a43e56e7..cbbf59caf 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -12,30 +12,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { - [Fact] - public void ConstructorAssignsProperties() - { - var s = new Size(1, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - Assert.Equal(s, builder.Size); - } - - [Fact] - public void ConstructorThrowsInvalid() - { - Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); - - Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s)); - }); - } - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); @@ -44,12 +20,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); + protected override Vector2 Execute( AffineTransformBuilder builder, Rectangle rectangle, Vector2 sourcePoint) { - Matrix3x2 matrix = builder.BuildMatrix(); + Matrix3x2 matrix = builder.BuildMatrix(rectangle); return Vector2.Transform(sourcePoint, matrix); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index f86db5641..0c167d7f9 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler resampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); @@ -101,12 +101,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { image.DebugSave(provider, $"_original"); - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); - this.PrintMatrix(builder.BuildMatrix()); + this.PrintMatrix(builder.BuildMatrix(image.Size())); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); @@ -159,10 +159,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { image.DebugSave(provider, $"_original"); - AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(2, 1.5F)); - image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); + image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -178,10 +178,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(rectangle) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(1F, 2F)); - image.Mutate(i => i.Transform(builder, KnownResamplers.Spline)); + image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); @@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - AffineTransformBuilder builder = new AffineTransformBuilder(image.Size()) + AffineTransformBuilder builder = new AffineTransformBuilder() .AppendCenteredRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index f467bed40..14ed57033 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -35,6 +35,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms }); } + protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(rectangle); + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 3f259c9aa..648ed5861 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -129,9 +129,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(p1, p2, Comparer); } + [Theory] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(-1, 0)] + public void ThrowsForInvalidSizes(int width, int height) + { + var size = new Size(width, height); + + Assert.ThrowsAny( + () => + { + TBuilder builder = this.CreateBuilder(size); + this.Execute(builder, new Rectangle(Point.Empty, size), Vector2.Zero); + }); + } + protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size)); - protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle); + protected abstract TBuilder CreateBuilder(Rectangle rectangle); protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void AppendScale(TBuilder builder, SizeF scale); From eab8a5677c73074f68cb369814a27beaf2e57641 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:19:53 +0100 Subject: [PATCH 35/66] fix rotate and skew processors --- .../Processors/Transforms/RotateProcessor.cs | 10 +++++++++- .../Processing/Processors/Transforms/SkewProcessor.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 57cca4bf9..69ecf5921 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; + using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.ParallelUtils; @@ -34,12 +36,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the rotating operation. /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) - : base( + : this( TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; + // Helper constructor + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, rotationMatrix)) + { + } + /// /// Gets the angle of rotation in degrees. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 4a006a9df..c7b1d7410 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Numerics; + using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -32,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The sampler to perform the skew operation. /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) - : base( + : this( TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize), sampler, sourceSize) @@ -41,6 +43,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.DegreesY = degreesY; } + // Helper constructor: + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, skewMatrix)) + { + } + /// /// Gets the angle of rotation along the x-axis in degrees. /// From 81e16e48a3141ecd1e5fdfd62ab07cc6449600bb Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 01:29:31 +0100 Subject: [PATCH 36/66] StyleCop --- .../Processors/Transforms/AffineTransformProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index f357b6c27..aabaa63cf 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = targetDimensions;; + this.TargetDimensions = targetDimensions; } /// From bbfb2a0ef9249183bce9f8128d4ad058361a8ef2 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 02:37:10 +0100 Subject: [PATCH 37/66] Rotation around custom center point. Rename PrependCenteredRotationRadians back to PrependRotationRadians --- .../Processing/AffineTransformBuilder.cs | 40 ++++++++++++---- .../Processing/ProjectiveTransformBuilder.cs | 35 ++++++++++++-- .../ImageSharp.Tests/Drawing/DrawImageTest.cs | 2 +- .../Transforms/AffineTransformBuilderTests.cs | 8 +++- .../Transforms/AffineTransformTests.cs | 8 ++-- .../ProjectiveTransformBuilderTests.cs | 8 +++- .../Transforms/TransformBuilderTestBase.cs | 46 +++++++++++++++++-- 7 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 42d555f20..eab75cfdb 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -20,33 +20,55 @@ namespace SixLabors.ImageSharp.Processing /// Prepends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center point /// The . - public AffineTransformBuilder PrependCenteredRotationRadians(float radians) - => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + => this.PrependMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center point + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) + => this.AppendMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); + + /// + /// Prepends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. + /// The . + public AffineTransformBuilder PrependRotationRadians(float radians) + => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + + /// + /// Appends a rotation matrix using the given rotation angle in radians + /// and the image center point as rotation center. + /// + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendCenteredRotationRadians(float radians) + public AffineTransformBuilder AppendRotationRadians(float radians) => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public AffineTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Appends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendCenteredRotationDegrees(float degrees) - => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 9bcbaf834..8bde90d51 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -67,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public ProjectiveTransformBuilder PrependCenteredRotationRadians(float radians) + public ProjectiveTransformBuilder PrependRotationRadians(float radians) { var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); return this.PrependMatrix(m); @@ -78,27 +79,51 @@ namespace SixLabors.ImageSharp.Processing /// /// The amount of rotation, in radians. /// The . - public ProjectiveTransformBuilder AppendCenteredRotationRadians(float radians) + public ProjectiveTransformBuilder AppendRotationRadians(float radians) { var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); return this.AppendMatrix(m); } + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation center. + /// The . + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + { + var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); + return this.PrependMatrix(m); + } + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation center. + /// The . + internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) + { + var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); + return this.AppendMatrix(m); + } + /// /// Prepends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. /// /// The amount of rotation, in degrees. /// The . - public ProjectiveTransformBuilder AppendCenteredRotationDegrees(float degrees) - => this.AppendCenteredRotationRadians(ImageMaths.DegreesToRadians(degrees)); + public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs index f3772e3aa..374454afb 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTest.cs @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Tests using (var blend = Image.Load(TestFile.Create(TestImages.Bmp.Car).Bytes)) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(45F) + .AppendRotationDegrees(45F) .AppendScale(new SizeF(.25F, .25F)) .AppendTranslation(new PointF(10, 10)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index cbbf59caf..e02585168 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -14,11 +14,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => + builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => + builder.PrependRotationRadians(radians, center); protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index 0c167d7f9..ed6d3ef2b 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(30); + .AppendRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(angleDeg) + .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); @@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(angleDeg) + .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { AffineTransformBuilder builder = new AffineTransformBuilder() - .AppendCenteredRotationDegrees(50) + .AppendRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 14ed57033..01448ac59 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -39,11 +39,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => + builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => + builder.PrependRotationRadians(radians, center); protected override Vector2 Execute( ProjectiveTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 648ed5861..badf43013 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -87,7 +87,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms [InlineData(200, 100, 10, 42, 84)] [InlineData(200, 100, 100, 42, 84)] [InlineData(100, 200, -10, 42, 84)] - public void RotateDegrees_ShouldCreateCenteredMatrix(int width, int height, float deg, float x, float y) + public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter( + int width, + int height, + float deg, + float x, + float y) { var size = new Size(width, height); TBuilder builder = this.CreateBuilder(size); @@ -97,13 +102,41 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); - var position = new Vector2(x, y); + var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); Assert.Equal(actual, expected, Comparer); } - + + [Theory] + [InlineData(200, 100, 10, 30, 61, 42, 84)] + [InlineData(200, 100, 100, 30, 10, 20, 84)] + [InlineData(100, 200, -10, 30, 20, 11, 84)] + public void AppendRotationDegrees_WithRotationCenter( + int width, + int height, + float deg, + float cx, + float cy, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + var centerPoint = new Vector2(cx, cy); + this.AppendRotationDegrees(builder, deg, centerPoint); + + var matrix = Matrix3x2.CreateRotation(ImageMaths.DegreesToRadians(deg), centerPoint); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + [Fact] public void AppendPrependOpposite() { @@ -116,10 +149,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // Forwards this.AppendRotationRadians(b1, pi); this.AppendScale(b1, new SizeF(2, 0.5f)); + this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f)); this.AppendTranslation(b1, new PointF(123, 321)); // Backwards this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependScale(b2, new SizeF(2, 0.5f)); this.PrependRotationRadians(b2, pi); @@ -152,14 +187,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void AppendScale(TBuilder builder, SizeF scale); protected abstract void AppendRotationRadians(TBuilder builder, float radians); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); protected abstract void PrependTranslation(TBuilder builder, PointF translate); protected abstract void PrependScale(TBuilder builder, SizeF scale); protected abstract void PrependRotationRadians(TBuilder builder, float radians); + protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); + protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => + this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees), center); + protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); private static float Sqrt(float a) => (float)Math.Sqrt(a); From 0c9d6bbec115cd91448050bf8bc942ffb5c927fc Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 16:11:38 +0100 Subject: [PATCH 38/66] printing ReferenceKernelMap --- .../KernelMapTests.ReferenceKernelMap.cs | 9 ++++- .../Processors/Transforms/KernelMapTests.cs | 39 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 932ea5494..cf0e94e8b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms /// /// Simplified reference implementation for functionality. /// - public class ReferenceKernelMap + internal class ReferenceKernelMap { private readonly ReferenceKernel[] kernels; @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } - public struct ReferenceKernel + internal struct ReferenceKernel { public ReferenceKernel(int left, float[] values) { @@ -94,6 +94,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public float[] Values { get; } public int Length => this.Values.Length; + + public static implicit operator ReferenceKernel(ResizeKernel orig) + { + return new ReferenceKernel(orig.Left, orig.GetValues().ToArray()); + } } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 7abc1c312..555ed1567 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos3), 8, 6 }, // TODO: What's wrong here: - // { nameof(KnownResamplers.Lanczos3), 20, 12 }, + { nameof(KnownResamplers.Lanczos3), 20, 12 }, {nameof(KnownResamplers.Lanczos8), 500, 200 }, {nameof(KnownResamplers.Lanczos8), 100, 10 }, @@ -61,8 +61,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); #if DEBUG - string text = PrintKernelMap(kernelMap); - this.Output.WriteLine(text); + this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); + this.Output.WriteLine($"Reference KernelMap:\n{PrintKernelMap(referenceMap)}\n"); #endif for (int i = 0; i < kernelMap.DestinationSize; i++) @@ -73,19 +73,42 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.Equal(referenceKernel.Length, kernel.Length); Assert.Equal(referenceKernel.Left, kernel.Left); - Assert.True(kernel.GetValues().SequenceEqual(referenceKernel.Values)); + float[] expectedValues = referenceKernel.Values; + Span actualValues = kernel.GetValues(); + + Assert.Equal(expectedValues.Length, actualValues.Length); + + var comparer = new ApproximateFloatComparer(1e-6f); + + for (int x = 0; x < expectedValues.Length; x++) + { + Assert.True( + comparer.Equals(expectedValues[x], actualValues[x]), + $"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})"); + } } } - private static string PrintKernelMap(KernelMap kernelMap) + private static string PrintKernelMap(KernelMap kernelMap) => + PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); + + private static string PrintKernelMap(ReferenceKernelMap kernelMap) => + PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); + + private static string PrintKernelMap( + TKernelMap kernelMap, + Func getDestinationSize, + Func getKernel) { var bld = new StringBuilder(); - for (int i = 0; i < kernelMap.DestinationSize; i++) + int destinationSize = getDestinationSize(kernelMap); + + for (int i = 0; i < destinationSize; i++) { - ResizeKernel kernel = kernelMap.GetKernel(i); + ReferenceKernel kernel = getKernel(kernelMap, i); bld.Append($"({kernel.Left:D3}) || "); - Span span = kernel.GetValues(); + Span span = kernel.Values; for (int j = 0; j < kernel.Length; j++) { From eb82f9af683dc67e5b7d6c63f0d925273a640c38 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 26 Nov 2018 16:48:29 +0100 Subject: [PATCH 39/66] fixed a bug in KernelMap caused by overlapping memory areas --- .../Processors/Transforms/KernelMap.cs | 11 +++++++++-- .../Processors/Transforms/KernelMapTests.cs | 16 +++++++--------- .../Processing/Processors/Transforms/SkewTest.cs | 14 +------------- .../ImageSharp.Tests/TestUtilities/TestUtils.cs | 13 +++++++++++++ 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index 3f984fef0..3f2bf8dda 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.periodicRegionMin = period + radius; this.periodicRegionMax = destinationSize - radius; - int width = radius * 2; + int width = (radius * 2) + 1; this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); this.kernels = new ResizeKernel[destinationSize]; } @@ -141,8 +141,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// private ResizeKernel CreateKernel(int destIdx, int left, int rightIdx) { - int flatStartIndex = destIdx * this.data.Width; int length = rightIdx - left + 1; + + if (length > this.data.Width) + { + throw new InvalidOperationException($"Error in KernelMap.CreateKernel({destIdx},{left},{rightIdx}): left > this.data.Width"); + } + + int flatStartIndex = destIdx * this.data.Width; + Memory bufferSlice = this.data.Memory.Slice(flatStartIndex, length); return new ResizeKernel(left, bufferSlice); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 555ed1567..c5916ce0b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Text; using SixLabors.ImageSharp.PixelFormats; @@ -40,25 +41,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos3), 9, 12 }, { nameof(KnownResamplers.Lanczos3), 6, 8 }, { nameof(KnownResamplers.Lanczos3), 8, 6 }, - - // TODO: What's wrong here: { nameof(KnownResamplers.Lanczos3), 20, 12 }, - {nameof(KnownResamplers.Lanczos8), 500, 200 }, - {nameof(KnownResamplers.Lanczos8), 100, 10 }, - {nameof(KnownResamplers.Lanczos8), 100, 80 }, - {nameof(KnownResamplers.Lanczos8), 10, 100 }, + { nameof(KnownResamplers.Lanczos8), 500, 200 }, + { nameof(KnownResamplers.Lanczos8), 100, 10 }, + { nameof(KnownResamplers.Lanczos8), 100, 80 }, + { nameof(KnownResamplers.Lanczos8), 10, 100 }, }; [Theory] [MemberData(nameof(KernelMapData))] public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { - var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); - - var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); + IResampler resampler = TestUtils.GetResampler(resamplerName); var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); + var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); #if DEBUG this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs index d1d2ea077..29c51543f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { foreach (string resamplerName in ResamplerNames) { - IResampler sampler = GetResampler(resamplerName); + IResampler sampler = TestUtils.GetResampler(resamplerName); using (Image image = provider.GetImage()) { image.Mutate(i => i.Skew(x, y, sampler)); @@ -68,17 +68,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } } - - private static IResampler GetResampler(string name) - { - PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); - - if (property is null) - { - throw new Exception($"No resampler named '{name}"); - } - - return (IResampler)property.GetValue(null); - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index d7755ff7a..5ea0bccf0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; @@ -284,5 +285,17 @@ namespace SixLabors.ImageSharp.Tests } public static string AsInvariantString(this FormattableString formattable) => System.FormattableString.Invariant(formattable); + + public static IResampler GetResampler(string name) + { + PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); + + if (property is null) + { + throw new Exception($"No resampler named '{name}"); + } + + return (IResampler)property.GetValue(null); + } } } \ No newline at end of file From 5d034d5fc0f51d593a7ae2b7a3b827308d3b9415 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 18:16:47 +0000 Subject: [PATCH 40/66] Use existing method. (There's precedence) --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 14 +------------- .../Processing/AffineTransformBuilder.cs | 4 ++-- .../Processing/ProjectiveTransformBuilder.cs | 5 ++--- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index c0c8e07e6..8720140e1 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -125,19 +125,7 @@ namespace SixLabors.ImageSharp /// should be power of 2. /// [MethodImpl(InliningOptions.ShortMethod)] - public static int ModuloP2(int x, int m) - { - return x & (m - 1); - } - - /// - /// Converts degrees to radians - /// - [MethodImpl(InliningOptions.ShortMethod)] - public static float DegreesToRadians(float degrees) - { - return degrees * ((float)Math.PI / 180f); - } + public static int ModuloP2(int x, int m) => x & (m - 1); /// /// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation. diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index eab75cfdb..a91d1f0d2 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Appends a rotation matrix using the given rotation angle in degrees @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 8bde90d51..ad6ef20bb 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -115,7 +114,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -123,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(ImageMaths.DegreesToRadians(degrees)); + => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. From cfa7cfd3db4ecdd9d62e7ae14cfc4221dca99473 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 18:22:39 +0000 Subject: [PATCH 41/66] Remove "Matrix" suffix --- src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 4 ++-- .../Processing/Transforms/ProjectiveTransformTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index ad6ef20bb..dff9bc6d9 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing /// An enumeration that indicates on which corners to taper the rectangle. /// The amount to taper. /// The . - public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// An enumeration that indicates on which corners to taper the rectangle. /// The amount to taper. /// The . - public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction) + public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 0362d75a0..9436b7828 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) - .AppendTaperMatrix(TaperSide.Right, TaperCorner.Both, .5F); + .AppendTaper(TaperSide.Right, TaperCorner.Both, .5F); image.Mutate(i => i.Transform(builder, sampler)); @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) - .AppendTaperMatrix(taperSide, taperCorner, .5F); + .AppendTaper(taperSide, taperCorner, .5F); image.Mutate(i => i.Transform(builder)); From ffba817bcae3a2895534c037798bf8e30232e06d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 26 Nov 2018 22:59:31 +0000 Subject: [PATCH 42/66] Update dependencies --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 4 ++-- .../Converters/CIeLchToCieLabConverter.cs | 2 +- .../Converters/CieLabToCieLchConverter.cs | 2 +- .../Converters/CieLchuvToCieLuvConverter.cs | 2 +- .../Converters/CieLuvToCieLchuvConverter.cs | 2 +- src/ImageSharp/ImageSharp.csproj | 2 +- src/ImageSharp/Processing/AffineTransformBuilder.cs | 4 ++-- src/ImageSharp/Processing/KnownFilterMatrices.cs | 2 +- .../Processing/ProjectiveTransformBuilder.cs | 9 ++------- .../Processing/Transforms/TransformBuilderTestBase.cs | 10 ++++------ 10 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 1cb3f444f..ae5069be1 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -38,8 +38,8 @@ - - + + All diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs index dd352db80..40d8c5bc6 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation // Conversion algorithm described here: // https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC float l = input.L, c = input.C, hDegrees = input.H; - float hRadians = MathFExtensions.DegreeToRadian(hDegrees); + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); float a = c * MathF.Cos(hRadians); float b = c * MathF.Sin(hRadians); diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs index 81196604e..2b859205a 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation float l = input.L, a = input.A, b = input.B; float c = MathF.Sqrt((a * a) + (b * b)); float hRadians = MathF.Atan2(b, a); - float hDegrees = MathFExtensions.RadianToDegree(hRadians); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); // Wrap the angle round at 360. hDegrees %= 360; diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs index 4f5a20bec..ba5b8bfb7 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation // Conversion algorithm described here: // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29 float l = input.L, c = input.C, hDegrees = input.H; - float hRadians = MathFExtensions.DegreeToRadian(hDegrees); + float hRadians = GeometryUtilities.DegreeToRadian(hDegrees); float u = c * MathF.Cos(hRadians); float v = c * MathF.Sin(hRadians); diff --git a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs index 297c18c5c..3c7d356a5 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation float l = input.L, a = input.U, b = input.V; float c = MathF.Sqrt((a * a) + (b * b)); float hRadians = MathF.Atan2(b, a); - float hDegrees = MathFExtensions.RadianToDegree(hRadians); + float hDegrees = GeometryUtilities.RadianToDegree(hRadians); // Wrap the angle round at 360. hDegrees %= 360; diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 29d29d50d..c19c92426 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -42,7 +42,7 @@ - + All diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index a91d1f0d2..ed5aa987f 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a rotation matrix using the given rotation angle in degrees @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/src/ImageSharp/Processing/KnownFilterMatrices.cs b/src/ImageSharp/Processing/KnownFilterMatrices.cs index 4f5e3c869..cf0d19ff8 100644 --- a/src/ImageSharp/Processing/KnownFilterMatrices.cs +++ b/src/ImageSharp/Processing/KnownFilterMatrices.cs @@ -322,7 +322,7 @@ namespace SixLabors.ImageSharp.Processing degrees += 360; } - float radian = MathFExtensions.DegreeToRadian(degrees); + float radian = GeometryUtilities.DegreeToRadian(degrees); float cosRadian = MathF.Cos(radian); float sinRadian = MathF.Sin(radian); diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index dff9bc6d9..750f2f5d0 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in degrees. /// The . public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(MathFExtensions.DegreeToRadian(degrees)); + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a scale matrix from the given uniform scale. @@ -247,10 +247,5 @@ namespace SixLabors.ImageSharp.Processing return matrix; } - - /// - /// Removes all matrices from the builder. - /// - public void Clear() => this.matrices.Clear(); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index badf43013..dffa3c7ac 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -3,8 +3,6 @@ using System; using System.Numerics; - -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; @@ -128,7 +126,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms var centerPoint = new Vector2(cx, cy); this.AppendRotationDegrees(builder, deg, centerPoint); - var matrix = Matrix3x2.CreateRotation(ImageMaths.DegreesToRadians(deg), centerPoint); + var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(deg), centerPoint); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -195,13 +193,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees)); + this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => - this.AppendRotationRadians(builder, ImageMaths.DegreesToRadians(degrees), center); + this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); - + private static float Sqrt(float a) => (float)Math.Sqrt(a); } } \ No newline at end of file From 7ea99129b5745fd780b11d4f84a959d1cdb5c406 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 00:46:32 +0000 Subject: [PATCH 43/66] Match APIs --- .../Processing/AffineTransformBuilder.cs | 114 +++++----- .../ProjectiveTransformProcessor.cs | 6 +- .../Processing/ProjectiveTransformBuilder.cs | 210 ++++++++---------- .../Processing/TransformExtensions.cs | 96 +++++--- .../Transforms/AffineTransformBuilderTests.cs | 15 +- .../ProjectiveTransformBuilderTests.cs | 40 +--- .../Transforms/ProjectiveTransformTests.cs | 6 +- .../Transforms/TransformBuilderTestBase.cs | 12 +- 8 files changed, 246 insertions(+), 253 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ed5aa987f..29c37d7bf 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -17,22 +17,13 @@ namespace SixLabors.ImageSharp.Processing private readonly List> matrixFactories = new List>(); /// - /// Prepends a centered rotation matrix using the given rotation in radians. - /// - /// The amount of rotation, in radians. - /// The rotation center point - /// The . - public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - => this.PrependMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); - - /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Prepends a rotation matrix using the given rotation angle in degrees + /// and the image center point as rotation center. /// - /// The amount of rotation, in radians. - /// The rotation center point + /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - => this.AppendMatrix(Matrix3x2.CreateRotation(radians, centerPoint)); + public AffineTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a rotation matrix using the given rotation angle in radians @@ -44,31 +35,40 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Appends a rotation matrix using the given rotation angle in radians - /// and the image center point as rotation center. + /// Prepends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation origin point. /// The . - public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateRotation(radians, origin)); /// - /// Prepends a rotation matrix using the given rotation angle in degrees + /// Appends a rotation matrix using the given rotation angle in degrees /// and the image center point as rotation center. /// /// The amount of rotation, in degrees. /// The . - public AffineTransformBuilder PrependRotationDegrees(float degrees) - => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + public AffineTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// - /// Appends a rotation matrix using the given rotation angle in degrees + /// Appends a rotation matrix using the given rotation angle in radians /// and the image center point as rotation center. /// - /// The amount of rotation, in degrees. + /// The amount of rotation, in radians. /// The . - public AffineTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + public AffineTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); + + /// + /// Appends a centered rotation matrix using the given rotation in radians. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateRotation(radians, origin)); /// /// Prepends a scale matrix from the given uniform scale. @@ -79,12 +79,12 @@ namespace SixLabors.ImageSharp.Processing => this.PrependMatrix(Matrix3x2.CreateScale(scale)); /// - /// Appends a scale matrix from the given uniform scale. + /// Prepends a scale matrix from the given vector scale. /// - /// The uniform scale. + /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScale(float scale) - => this.AppendMatrix(Matrix3x2.CreateScale(scale)); + public AffineTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); /// /// Prepends a scale matrix from the given vector scale. @@ -95,28 +95,28 @@ namespace SixLabors.ImageSharp.Processing => this.PrependMatrix(Matrix3x2.CreateScale(scales)); /// - /// Appends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given uniform scale. /// - /// The horizontal and vertical scale. + /// The uniform scale. /// The . - public AffineTransformBuilder AppendScale(Vector2 scales) - => this.AppendMatrix(Matrix3x2.CreateScale(scales)); + public AffineTransformBuilder AppendScale(float scale) + => this.AppendMatrix(Matrix3x2.CreateScale(scale)); /// - /// Prepends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given vector scale. /// - /// The horizontal and vertical scale. + /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder PrependScale(SizeF scale) - => this.PrependScale((Vector2)scale); + public AffineTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. /// The . - public AffineTransformBuilder AppendScale(SizeF scales) - => this.AppendScale((Vector2)scales); + public AffineTransformBuilder AppendScale(Vector2 scales) + => this.AppendMatrix(Matrix3x2.CreateScale(scales)); /// /// Prepends a centered skew matrix from the give angles in degrees. @@ -141,66 +141,58 @@ namespace SixLabors.ImageSharp.Processing /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(Vector2 position) - => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// - /// Appends a translation matrix from the given vector. + /// Prepends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); + public AffineTransformBuilder PrependTranslation(Vector2 position) + => this.PrependMatrix(Matrix3x2.CreateTranslation(position)); /// - /// Prepends a translation matrix from the given vector. + /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. /// The . - public AffineTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + public AffineTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix3x2.CreateTranslation(position)); /// /// Prepends a raw matrix. /// /// The matrix to prepend. /// The . - public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) - { - this.matrixFactories.Insert(0, _ => matrix); - return this; - } + public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) => this.Prepend(_ => matrix); /// /// Appends a raw matrix. /// /// The matrix to append. /// The . - public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) - { - this.matrixFactories.Add(_ => matrix); - return this; - } + public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) => this.Append(_ => matrix); /// /// Returns the combined matrix for a given source size. /// - /// The source image size + /// The source image size. /// The . public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); /// /// Returns the combined matrix for a given source rectangle. /// - /// The rectangle in the source image + /// The rectangle in the source image. /// The . public Matrix3x2 BuildMatrix(Rectangle sourceRectangle) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index bfde1769c..273156e2e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -24,13 +24,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The transform matrix. /// The sampler to perform the transform operation. - /// The source image size. - public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize) + /// The target dimensions. + public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions) { Guard.NotNull(sampler, nameof(sampler)); this.Sampler = sampler; this.TransformMatrix = matrix; - this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix); + this.TargetDimensions = targetDimensions; } /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 750f2f5d0..a905467bb 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Numerics; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -13,34 +14,7 @@ namespace SixLabors.ImageSharp.Processing /// public class ProjectiveTransformBuilder { - private readonly List matrices = new List(); - private Rectangle sourceRectangle; - - /// - /// Initializes a new instance of the class. - /// - /// The source image size. - public ProjectiveTransformBuilder(Size sourceSize) - : this(new Rectangle(Point.Empty, sourceSize)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The source rectangle. - public ProjectiveTransformBuilder(Rectangle sourceRectangle) - { - Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); - Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); - - this.sourceRectangle = sourceRectangle; - } - - /// - /// Gets the source image size. - /// - internal Size Size => this.sourceRectangle.Size; + private readonly List> matrixFactories = new List>(); /// /// Prepends a matrix that performs a tapering projective transform. @@ -50,7 +24,7 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) - => this.PrependMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -60,7 +34,15 @@ namespace SixLabors.ImageSharp.Processing /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) - => this.AppendMatrix(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction)); + => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + + /// + /// Prepends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Prepends a centered rotation matrix using the given rotation in radians. @@ -68,33 +50,32 @@ namespace SixLabors.ImageSharp.Processing /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - { - var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); - return this.PrependMatrix(m); - } + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. + /// The rotation center. /// The . - public ProjectiveTransformBuilder AppendRotationRadians(float radians) - { - var m = new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, this.Size)); - return this.AppendMatrix(m); - } + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) + => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + + /// + /// Appends a centered rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// The . + public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); /// /// Appends a centered rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. - /// The rotation center. /// The . - internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - { - var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); - return this.PrependMatrix(m); - } + public ProjectiveTransformBuilder AppendRotationRadians(float radians) + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in radians. @@ -103,80 +84,69 @@ namespace SixLabors.ImageSharp.Processing /// The rotation center. /// The . internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - { - var m = Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0)); - return this.AppendMatrix(m); - } + => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); /// - /// Prepends a centered rotation matrix using the given rotation in degrees. + /// Prepends a scale matrix from the given uniform scale. /// - /// The amount of rotation, in degrees. - /// The . - public ProjectiveTransformBuilder PrependCenteredRotationDegrees(float degrees) - => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + /// The uniform scale. + /// The . + public ProjectiveTransformBuilder PrependScale(float scale) + => this.PrependMatrix(Matrix4x4.CreateScale(scale)); /// - /// Appends a centered rotation matrix using the given rotation in degrees. + /// Prepends a scale matrix from the given vector scale. /// - /// The amount of rotation, in degrees. - /// The . - public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) - => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees)); + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(SizeF scale) + => this.PrependScale((Vector2)scale); /// - /// Prepends a scale matrix from the given uniform scale. + /// Prepends a scale matrix from the given vector scale. /// - /// The uniform scale. - /// The . - public ProjectiveTransformBuilder PrependScale(float scale) - => this.PrependMatrix(Matrix4x4.CreateScale(scale)); + /// The horizontal and vertical scale. + /// The . + public ProjectiveTransformBuilder PrependScale(Vector2 scales) + => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); /// /// Appends a scale matrix from the given uniform scale. /// /// The uniform scale. - /// The . + /// The . public ProjectiveTransformBuilder AppendScale(float scale) => this.AppendMatrix(Matrix4x4.CreateScale(scale)); /// - /// Prepends a scale matrix from the given vector scale. + /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder PrependScale(Vector2 scales) - => this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); + /// The . + public ProjectiveTransformBuilder AppendScale(SizeF scales) + => this.AppendScale((Vector2)scales); /// /// Appends a scale matrix from the given vector scale. /// /// The horizontal and vertical scale. - /// The . + /// The . public ProjectiveTransformBuilder AppendScale(Vector2 scales) - => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1f))); - - /// - /// Prepends a scale matrix from the given vector scale. - /// - /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder PrependScale(SizeF scale) - => this.PrependScale((Vector2)scale); + => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); /// - /// Appends a scale matrix from the given vector scale. + /// Prepends a translation matrix from the given vector. /// - /// The horizontal and vertical scale. - /// The . - public ProjectiveTransformBuilder AppendScale(SizeF scales) - => this.AppendScale((Vector2)scales); + /// The translation position. + /// The . + public ProjectiveTransformBuilder PrependTranslation(PointF position) + => this.PrependTranslation((Vector2)position); /// /// Prepends a translation matrix from the given vector. /// /// The translation position. - /// The . + /// The . public ProjectiveTransformBuilder PrependTranslation(Vector2 position) => this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); @@ -184,68 +154,72 @@ namespace SixLabors.ImageSharp.Processing /// Appends a translation matrix from the given vector. /// /// The translation position. - /// The . - public ProjectiveTransformBuilder AppendTranslation(Vector2 position) - => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); - - /// - /// Prepends a translation matrix from the given vector. - /// - /// The translation position. - /// The . - public ProjectiveTransformBuilder PrependTranslation(PointF position) - => this.PrependTranslation((Vector2)position); + /// The . + public ProjectiveTransformBuilder AppendTranslation(PointF position) + => this.AppendTranslation((Vector2)position); /// /// Appends a translation matrix from the given vector. /// /// The translation position. - /// The . - public ProjectiveTransformBuilder AppendTranslation(PointF position) - => this.AppendTranslation((Vector2)position); + /// The . + public ProjectiveTransformBuilder AppendTranslation(Vector2 position) + => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); /// /// Prepends a raw matrix. /// /// The matrix to prepend. /// The . - public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) - { - this.matrices.Insert(0, matrix); - return this; - } + public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) => this.Prepend(_ => matrix); /// /// Appends a raw matrix. /// /// The matrix to append. /// The . - public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) - { - this.matrices.Add(matrix); - return this; - } + public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) => this.Append(_ => matrix); + + /// + /// Returns the combined matrix for a given source size. + /// + /// The source image size. + /// The . + public Matrix4x4 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); /// - /// Returns the combined matrix. + /// Returns the combined matrix for a given source rectangle. /// + /// The rectangle in the source image. /// The . - public Matrix4x4 BuildMatrix() + public Matrix4x4 BuildMatrix(Rectangle sourceRectangle) { - Matrix4x4 matrix = Matrix4x4.Identity; + Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle)); + Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle)); // Translate the origin matrix to cater for source rectangle offsets. - if (!this.sourceRectangle.Equals(default)) - { - matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.sourceRectangle.Location, 0)); - } + var matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); + + Size size = sourceRectangle.Size; - foreach (Matrix4x4 m in this.matrices) + foreach (Func factory in this.matrixFactories) { - matrix *= m; + matrix *= factory(size); } return matrix; } + + private ProjectiveTransformBuilder Prepend(Func factory) + { + this.matrixFactories.Insert(0, factory); + return this; + } + + private ProjectiveTransformBuilder Append(Func factory) + { + this.matrixFactories.Add(factory); + return this; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/TransformExtensions.cs b/src/ImageSharp/Processing/TransformExtensions.cs index de4a3e5ba..db14b6baf 100644 --- a/src/ImageSharp/Processing/TransformExtensions.cs +++ b/src/ImageSharp/Processing/TransformExtensions.cs @@ -15,40 +15,32 @@ namespace SixLabors.ImageSharp.Processing public static class TransformExtensions { /// - /// Performs an affine transform of an image using the specified sampling algorithm. + /// Performs an affine transform of an image. /// /// The pixel format. - /// The . - /// The source rectangle - /// The transformation matrix. - /// The size of the result image. - /// The to perform the resampling. + /// The image to transform. + /// The affine transform builder. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext ctx, - Rectangle sourceRectangle, - Matrix3x2 transform, - Size targetDimensions, - IResampler sampler) + this IImageProcessingContext source, + AffineTransformBuilder builder) where TPixel : struct, IPixel - { - return ctx.ApplyProcessor( - new AffineTransformProcessor(transform, sampler, targetDimensions), - sourceRectangle); - } + => Transform(source, builder, KnownResamplers.Bicubic); /// - /// Performs an affine transform of an image. + /// Performs an affine transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . /// The affine transform builder. + /// The to perform the resampling. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext source, - AffineTransformBuilder builder) + this IImageProcessingContext ctx, + AffineTransformBuilder builder, + IResampler sampler) where TPixel : struct, IPixel - => Transform(source, builder, KnownResamplers.Bicubic); + => ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); /// /// Performs an affine transform of an image using the specified sampling algorithm. @@ -76,16 +68,22 @@ namespace SixLabors.ImageSharp.Processing /// /// The pixel format. /// The . - /// The affine transform builder. + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. /// The to perform the resampling. /// The public static IImageProcessingContext Transform( this IImageProcessingContext ctx, - AffineTransformBuilder builder, + Rectangle sourceRectangle, + Matrix3x2 transform, + Size targetDimensions, IResampler sampler) where TPixel : struct, IPixel { - return ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + return ctx.ApplyProcessor( + new AffineTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); } /// @@ -105,15 +103,59 @@ namespace SixLabors.ImageSharp.Processing /// Performs a projective transform of an image using the specified sampling algorithm. /// /// The pixel format. - /// The image to transform. + /// The . /// The projective transform builder. /// The to perform the resampling. /// The public static IImageProcessingContext Transform( - this IImageProcessingContext source, + this IImageProcessingContext ctx, + ProjectiveTransformBuilder builder, + IResampler sampler) + where TPixel : struct, IPixel + => ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler); + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The projective transform builder. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, ProjectiveTransformBuilder builder, IResampler sampler) where TPixel : struct, IPixel - => source.ApplyProcessor(new ProjectiveTransformProcessor(builder.BuildMatrix(), sampler, builder.Size)); + { + Matrix4x4 transform = builder.BuildMatrix(sourceRectangle); + Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform); + return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler); + } + + /// + /// Performs a projective transform of an image using the specified sampling algorithm. + /// + /// The pixel format. + /// The . + /// The source rectangle + /// The transformation matrix. + /// The size of the result image. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform( + this IImageProcessingContext ctx, + Rectangle sourceRectangle, + Matrix4x4 transform, + Size targetDimensions, + IResampler sampler) + where TPixel : struct, IPixel + { + return ctx.ApplyProcessor( + new ProjectiveTransformProcessor(transform, sampler, targetDimensions), + sourceRectangle); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index e02585168..bdc66641a 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -1,28 +1,31 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; -using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => - builder.PrependRotationRadians(radians, center); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => + builder.PrependRotationRadians(radians, origin); protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); @@ -35,4 +38,4 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms return Vector2.Transform(sourcePoint, matrix); } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 01448ac59..287d7be49 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -1,60 +1,40 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Numerics; using SixLabors.ImageSharp.Processing; using SixLabors.Primitives; -using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class ProjectiveTransformBuilderTests : TransformBuilderTestBase { - [Fact] - public void ConstructorAssignsProperties() - { - var s = new Size(1, 1); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - Assert.Equal(s, builder.Size); - } - - [Fact] - public void ConstructorThrowsInvalid() - { - Assert.Throws(() => - { - var s = new Size(0, 1); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - }); - - Assert.Throws(() => - { - var s = new Size(1, 0); - var builder = new ProjectiveTransformBuilder(new Rectangle(Point.Empty, s)); - }); - } - - protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(rectangle); + protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(); protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => builder.AppendRotationRadians(radians, center); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); - protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => - builder.PrependRotationRadians(radians, center); + + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => + builder.PrependRotationRadians(radians, origin); protected override Vector2 Execute( ProjectiveTransformBuilder builder, Rectangle rectangle, Vector2 sourcePoint) { - Matrix4x4 matrix = builder.BuildMatrix(); + Matrix4x4 matrix = builder.BuildMatrix(rectangle); return Vector2.Transform(sourcePoint, matrix); } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 9436b7828..1da660d22 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms IResampler sampler = GetResampler(resamplerName); using (Image image = provider.GetImage()) { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendTaper(TaperSide.Right, TaperCorner.Both, .5F); image.Mutate(i => i.Transform(builder, sampler)); @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { using (Image image = provider.GetImage()) { - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendTaper(taperSide, taperCorner, .5F); image.Mutate(i => i.Transform(builder)); @@ -113,7 +113,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Matrix4x4 matrix = Matrix4x4.Identity; matrix.M13 = 0.01F; - ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size()) + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendMatrix(matrix); image.Mutate(i => i.Transform(builder)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index dffa3c7ac..720c87ced 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -18,7 +18,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms new TheoryData { // scale, translate, source, expectedDest - { Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero }, { Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) }, { Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) }, @@ -44,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms new TheoryData { // translate, scale, source, expectedDest - { Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero }, { Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) }, { new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) }, @@ -183,14 +181,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract TBuilder CreateBuilder(Rectangle rectangle); protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationRadians(TBuilder builder, float radians); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void PrependRotationRadians(TBuilder builder, float radians); - protected abstract void PrependRotationRadians(TBuilder b1, float v, Vector2 vector2); + + protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); @@ -199,7 +203,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); - - private static float Sqrt(float a) => (float)Math.Sqrt(a); } } \ No newline at end of file From a1b74ea1cb3e1fecddcb7af74cb4c48a46734060 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 10:15:52 +0000 Subject: [PATCH 44/66] Add large jpeg to 32bit skip --- tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 5977e59cf..15f7f92a8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -49,7 +49,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.NoEoiProgressive517, TestImages.Jpeg.Issues.BadRstProgressive518, TestImages.Jpeg.Issues.InvalidEOI695, - TestImages.Jpeg.Issues.ExifResizeOutOfRange696 + TestImages.Jpeg.Issues.ExifResizeOutOfRange696, + TestImages.Jpeg.Issues.ExifGetString750Transform }; return !TestEnvironment.Is64BitProcess && largeImagesToSkipOn32Bit.Contains(provider.SourceFileOrDescription); From 964ebcc1c30e965ab66be49c99f1dfad5d47e2b9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 10:23:06 +0000 Subject: [PATCH 45/66] Remove unused variables --- .../Processors/Transforms/AffineTransformProcessor.cs | 4 +--- .../Processors/Transforms/ProjectiveTransformProcessor.cs | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index aabaa63cf..5a3b5943b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -69,14 +69,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Handle tranforms that result in output identical to the original. if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity)) { - // The cloned will be blank here copy all the pixel data over + // The clone will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } - int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 273156e2e..98d488a42 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -65,14 +65,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Handle tranforms that result in output identical to the original. if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity)) { - // The cloned will be blank here copy all the pixel data over + // The clone will be blank here copy all the pixel data over source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); return; } - int height = this.TargetDimensions.Height; int width = this.TargetDimensions.Width; - var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions); // Convert from screen to world space. @@ -129,7 +127,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. - var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); From 35ff16d5a6cb4caf54df2eba7646dc4eabd393b3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 27 Nov 2018 12:12:42 +0000 Subject: [PATCH 46/66] Add Rotate(degrees, origin) --- .../Processing/AffineTransformBuilder.cs | 22 ++++++++++-- .../Processing/ProjectiveTransformBuilder.cs | 34 ++++++++++++++----- .../Transforms/AffineTransformBuilderTests.cs | 7 ++-- .../ProjectiveTransformBuilderTests.cs | 7 ++-- .../Transforms/TransformBuilderTestBase.cs | 8 ++--- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 29c37d7bf..ef0b24f81 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -35,7 +35,16 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Prepends a centered rotation matrix using the given rotation in radians. + /// Prepends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. /// The rotation origin point. @@ -62,7 +71,16 @@ namespace SixLabors.ImageSharp.Processing => this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size)); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in degrees. + /// The rotation origin point. + /// The . + public AffineTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. /// The rotation origin point. diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index a905467bb..5a19b890d 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -53,13 +53,22 @@ namespace SixLabors.ImageSharp.Processing => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin) + => this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Prepends a centered rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. - /// The rotation center. + /// The rotation origin point. /// The . - internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 centerPoint) - => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin) + => this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); /// /// Appends a centered rotation matrix using the given rotation in degrees. @@ -78,13 +87,22 @@ namespace SixLabors.ImageSharp.Processing => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size))); /// - /// Appends a centered rotation matrix using the given rotation in radians. + /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. + /// + /// The amount of rotation, in radians. + /// The rotation origin point. + /// The . + internal ProjectiveTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin) + => this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin); + + /// + /// Appends a centered rotation matrix using the given rotation in radians at the given origin. /// /// The amount of rotation, in radians. - /// The rotation center. + /// The rotation origin point. /// The . - internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 centerPoint) - => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(centerPoint, 0))); + internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 origin) + => this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); /// /// Prepends a scale matrix from the given uniform scale. diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index bdc66641a..14b867264 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -15,8 +15,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 center) => - builder.AppendRotationRadians(radians, center); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 287d7be49..9f58826cd 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -17,8 +17,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); - protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 center) => - builder.AppendRotationRadians(radians, center); + protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 720c87ced..43ad756e0 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -186,7 +186,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void AppendRotationRadians(TBuilder builder, float radians); - protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 center); + protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin); protected abstract void PrependTranslation(TBuilder builder, PointF translate); @@ -196,11 +196,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) => - this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees)); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); - protected virtual void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 center) => - this.AppendRotationRadians(builder, GeometryUtilities.DegreeToRadian(degrees), center); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); } From 65ce05d8c961358b4ca1543c901804ccb4a9c7cd Mon Sep 17 00:00:00 2001 From: Anton Firsov Date: Tue, 27 Nov 2018 21:37:57 +0100 Subject: [PATCH 47/66] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 188c11ff3..19941e65e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -20,6 +20,11 @@ * Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. +#### **Running tests and Debugging** + +* Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/master/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules! +* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1, because of JIT Code Generation bugs like dotnet/coreclr#16443 or dotnet/coreclr#20657 + #### **Do you have questions about consuming the library or the source code?** * Ask any question about how to use ImageSharp in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General). From 836738c5b5e1edf2dab63753e82ea0050e32f931 Mon Sep 17 00:00:00 2001 From: Anton Firsov Date: Tue, 27 Nov 2018 21:41:01 +0100 Subject: [PATCH 48/66] Fix links --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 19941e65e..01c09d223 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,7 +23,7 @@ #### **Running tests and Debugging** * Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/master/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules! -* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1, because of JIT Code Generation bugs like dotnet/coreclr#16443 or dotnet/coreclr#20657 +* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1, because of JIT Code Generation bugs like [dotnet/coreclr#16443](https://github.com/dotnet/coreclr/issues/16443) or [dotnet/coreclr#20657](https://github.com/dotnet/coreclr/issues/20657) #### **Do you have questions about consuming the library or the source code?** From 70206614fc55bc20d8512af821450aa090f82137 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 27 Nov 2018 20:44:35 +0100 Subject: [PATCH 49/66] refactor KernelMap initialization --- .../Processors/Transforms/KernelMap.cs | 94 +++++++++++-------- .../KernelMapTests.ReferenceKernelMap.cs | 4 +- .../Processors/Transforms/KernelMapTests.cs | 18 +++- .../Processors/Transforms/ResizeTests.cs | 31 +++++- 4 files changed, 102 insertions(+), 45 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs index 3f2bf8dda..96eb97649 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -15,28 +15,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal class KernelMap : IDisposable { - private readonly Buffer2D data; + private readonly IResampler sampler; - private readonly ResizeKernel[] kernels; + private readonly int sourceSize; + + private readonly float ratio; - private int period; + private readonly float scale; - private int radius; + private readonly int radius; - private int periodicRegionMin; + private readonly Buffer2D data; - private int periodicRegionMax; + private readonly ResizeKernel[] kernels; - private KernelMap(MemoryAllocator memoryAllocator, int destinationSize, int radius, int period) + private KernelMap( + MemoryAllocator memoryAllocator, + IResampler sampler, + int sourceSize, + int destinationSize, + int bufferHeight, + float ratio, + float scale, + int radius) { - this.DestinationSize = destinationSize; - this.period = period; + this.sampler = sampler; + this.ratio = ratio; + this.scale = scale; this.radius = radius; - this.periodicRegionMin = period + radius; - this.periodicRegionMax = destinationSize - radius; - - int width = (radius * 2) + 1; - this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); + this.sourceSize = sourceSize; + this.DestinationSize = destinationSize; + int maxWidth = (radius * 2) + 1; + this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean); this.kernels = new ResizeKernel[destinationSize]; } @@ -79,61 +89,69 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; - int radius = (int)MathF.Ceiling(scale * sampler.Radius); - var result = new KernelMap(memoryAllocator, destinationSize, radius, period); - for (int i = 0; i < destinationSize; i++) + var result = new KernelMap( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + destinationSize, + ratio, + scale, + radius); + + result.BasicInit(); + + return result; + } + + private void BasicInit() + { + for (int i = 0; i < this.DestinationSize; i++) { - float center = ((i + .5F) * ratio) - .5F; + float center = ((i + .5F) * this.ratio) - .5F; // Keep inside bounds. - int left = (int)MathF.Ceiling(center - radius); + int left = (int)MathF.Ceiling(center - this.radius); if (left < 0) { left = 0; } - int right = (int)MathF.Floor(center + radius); - if (right > sourceSize - 1) + int right = (int)MathF.Floor(center + this.radius); + if (right > this.sourceSize - 1) { - right = sourceSize - 1; + right = this.sourceSize - 1; } float sum = 0; - ResizeKernel ws = result.CreateKernel(i, left, right); - result.kernels[i] = ws; + ResizeKernel kernel = this.CreateKernel(i, left, right); + this.kernels[i] = kernel; - ref float weightsBaseRef = ref MemoryMarshal.GetReference(ws.GetValues()); + ref float kernelBaseRef = ref MemoryMarshal.GetReference(kernel.GetValues()); for (int j = left; j <= right; j++) { - float weight = sampler.GetValue((j - center) / scale); - sum += weight; + float value = this.sampler.GetValue((j - center) / this.scale); + sum += value; // weights[j - left] = weight: - Unsafe.Add(ref weightsBaseRef, j - left) = weight; + Unsafe.Add(ref kernelBaseRef, j - left) = value; } // Normalize, best to do it here rather than in the pixel loop later on. if (sum > 0) { - for (int w = 0; w < ws.Length; w++) + for (int w = 0; w < kernel.Length; w++) { // weights[w] = weights[w] / sum: - ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w); - wRef /= sum; + ref float kRef = ref Unsafe.Add(ref kernelBaseRef, w); + kRef /= sum; } } } - - return result; - } - - private int ReduceIndex(int destIndex) - { - return destIndex; } /// diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index cf0e94e8b..3786ec6e3 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex]; - public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize) + public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true) { float ratio = (float)sourceSize / destinationSize; float scale = ratio; @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms result.Add(new ReferenceKernel(left, values)); - if (sum > 0) + if (sum > 0 && normalize) { for (int w = 0; w < values.Length; w++) { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index c5916ce0b..69de5dbbf 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -35,6 +35,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Bicubic), 500, 200 }, { nameof(KnownResamplers.Bicubic), 200, 500 }, + { nameof(KnownResamplers.Bicubic), 10, 25 }, + { nameof(KnownResamplers.Lanczos3), 16, 12 }, { nameof(KnownResamplers.Lanczos3), 12, 16 }, { nameof(KnownResamplers.Lanczos3), 12, 9 }, @@ -43,12 +45,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos3), 8, 6 }, { nameof(KnownResamplers.Lanczos3), 20, 12 }, + { nameof(KnownResamplers.Lanczos3), 5, 25 }, + { nameof(KnownResamplers.Lanczos3), 5, 50 }, + { nameof(KnownResamplers.Lanczos8), 500, 200 }, { nameof(KnownResamplers.Lanczos8), 100, 10 }, { nameof(KnownResamplers.Lanczos8), 100, 80 }, { nameof(KnownResamplers.Lanczos8), 10, 100 }, }; + [Theory] + [MemberData(nameof(KernelMapData))] + public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) + { + IResampler resampler = TestUtils.GetResampler(resamplerName); + + var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false); + + this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); + } + [Theory] [MemberData(nameof(KernelMapData))] public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) @@ -60,7 +76,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms #if DEBUG this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); - this.Output.WriteLine($"Reference KernelMap:\n{PrintKernelMap(referenceMap)}\n"); + // this.Output.WriteLine($"Reference KernelMap:\n{PrintKernelMap(referenceMap)}\n"); #endif for (int i = 0; i < kernelMap.DestinationSize; i++) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index bec64e4d3..42cb083f1 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; +// ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { @@ -20,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); - public static readonly TheoryData AllReSamplers = + public static readonly TheoryData AllResamplers = new TheoryData { { "Bicubic", KnownResamplers.Bicubic }, @@ -40,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }; [Theory] - [WithTestPatternImages(nameof(AllReSamplers), 100, 100, DefaultPixelType, 0.5f)] - [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.5f)] - [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.3f)] + [WithTestPatternImages(nameof(AllResamplers), 100, 100, DefaultPixelType, 0.5f)] + [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.5f)] + [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.3f)] public void Resize_WorksWithAllResamplers(TestImageProvider provider, string name, IResampler sampler, float ratio) where TPixel : struct, IPixel { @@ -57,6 +58,28 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 1)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 10)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos3), 10)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos8), 10)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 10)] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 1)] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 5)] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 5)] + public void ScaleUp(TestImageProvider provider, string samplerName, float ratio) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + SizeF newSize = image.Size() * ratio; + image.Mutate(x => x.Resize((Size)newSize, TestUtils.GetResampler(samplerName), false)); + FormattableString details = $"{samplerName}_{ratio.ToString(System.Globalization.CultureInfo.InvariantCulture)}"; + + image.DebugSave(provider, details); + } + } + [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)] From e2ff889f45fbd56d8995ed516fab3388a59b9685 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 00:51:15 +0100 Subject: [PATCH 50/66] rename KernelMap to ResizeKernelMap, introduce MosaicKernelMap, move source code --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 3 +- src/ImageSharp/ImageSharp.csproj.DotSettings | 4 +- .../{ => Resamplers}/BicubicResampler.cs | 0 .../{ => Resamplers}/BoxResampler.cs | 0 .../{ => Resamplers}/CatmullRomResampler.cs | 0 .../{ => Resamplers}/HermiteResampler.cs | 0 .../{ => Resamplers}/Lanczos2Resampler.cs | 0 .../{ => Resamplers}/Lanczos3Resampler.cs | 0 .../{ => Resamplers}/Lanczos5Resampler.cs | 0 .../{ => Resamplers}/Lanczos8Resampler.cs | 0 .../MitchellNetravaliResampler.cs | 0 .../NearestNeighborResampler.cs | 0 .../{ => Resamplers}/RobidouxResampler.cs | 0 .../RobidouxSharpResampler.cs | 0 .../{ => Resamplers}/SplineResampler.cs | 0 .../{ => Resamplers}/TriangleResampler.cs | 0 .../{ => Resamplers}/WelchResampler.cs | 0 .../Transforms/{ => Resize}/ResizeKernel.cs | 22 +++--- .../Resize/ResizeKernelMap.MosaicKernelMap.cs | 46 ++++++++++++ .../ResizeKernelMap.cs} | 70 ++++++++++++------- .../{ => Resize}/ResizeProcessor.cs | 8 +-- .../KernelMapTests.ReferenceKernelMap.cs | 2 +- .../Processors/Transforms/KernelMapTests.cs | 4 +- .../Processors/Transforms/ResizeTests.cs | 1 + 24 files changed, 115 insertions(+), 45 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/BicubicResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/BoxResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/CatmullRomResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/HermiteResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/Lanczos2Resampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/Lanczos3Resampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/Lanczos5Resampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/Lanczos8Resampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/MitchellNetravaliResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/NearestNeighborResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/RobidouxResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/RobidouxSharpResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/SplineResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/TriangleResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resamplers}/WelchResampler.cs (100%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resize}/ResizeKernel.cs (81%) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs rename src/ImageSharp/Processing/Processors/Transforms/{KernelMap.cs => Resize/ResizeKernelMap.cs} (69%) rename src/ImageSharp/Processing/Processors/Transforms/{ => Resize}/ResizeProcessor.cs (98%) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index f07ccb03b..45cb52fd9 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -5,6 +5,7 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp @@ -98,7 +99,7 @@ namespace SixLabors.ImageSharp /// /// Determine the Least Common Multiple (LCM) of two numbers. - /// TODO: This method might be useful for building a more compact + /// TODO: This method might be useful for building a more compact /// public static int LeastCommonMultiple(int a, int b) { diff --git a/src/ImageSharp/ImageSharp.csproj.DotSettings b/src/ImageSharp/ImageSharp.csproj.DotSettings index cd75f91b7..a7337240a 100644 --- a/src/ImageSharp/ImageSharp.csproj.DotSettings +++ b/src/ImageSharp/ImageSharp.csproj.DotSettings @@ -5,4 +5,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs similarity index 81% rename from src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 1ce9c9c91..1183de754 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -11,18 +11,21 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Points to a collection of of weights allocated in . + /// Points to a collection of of weights allocated in . /// - internal struct ResizeKernel + internal unsafe struct ResizeKernel { + private readonly float* bufferPtr; + /// /// Initializes a new instance of the struct. /// [MethodImpl(InliningOptions.ShortMethod)] - internal ResizeKernel(int left, Memory bufferSlice) + internal ResizeKernel(int left, float* bufferPtr, int length) { this.Left = left; - this.BufferSlice = bufferSlice; + this.bufferPtr = bufferPtr; + this.Length = length; } /// @@ -30,22 +33,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// public int Left { get; } - /// - /// Gets the slice of the buffer containing the weights values. - /// - public Memory BufferSlice { get; } - /// /// Gets the the length of the kernel /// - public int Length => this.BufferSlice.Length; + public int Length { get; } /// - /// Gets the span representing the portion of the that this window covers + /// Gets the span representing the portion of the that this window covers /// /// The [MethodImpl(InliningOptions.ShortMethod)] - public Span GetValues() => this.BufferSlice.Span; + public Span GetValues() => new Span(this.bufferPtr, this.Length); /// /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs new file mode 100644 index 000000000..b815d05cb --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors.Transforms +{ + /// + /// Contains + /// + internal partial class ResizeKernelMap + { + /// + /// Memory-optimized where repeating rows are stored only once. + /// + private sealed class MosaicKernelMap : ResizeKernelMap + { + private readonly int period; + + private readonly int cornerInterval; + + public MosaicKernelMap( + MemoryAllocator memoryAllocator, + IResampler sampler, + int sourceSize, + int destinationSize, + float ratio, + float scale, + int radius, + int period, + int cornerInterval) + : base( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + (cornerInterval * 2) + period, + ratio, + scale, + radius) + { + this.cornerInterval = cornerInterval; + this.period = period; + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs similarity index 69% rename from src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 96eb97649..443db72d9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -11,9 +12,10 @@ using SixLabors.Memory; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Holds the values in an optimized contigous memory region. + /// Provides values from an optimized, + /// contigous memory region. /// - internal class KernelMap : IDisposable + internal partial class ResizeKernelMap : IDisposable { private readonly IResampler sampler; @@ -25,11 +27,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly int radius; + private readonly MemoryHandle pinHandle; + private readonly Buffer2D data; private readonly ResizeKernel[] kernels; - private KernelMap( + private ResizeKernelMap( MemoryAllocator memoryAllocator, IResampler sampler, int sourceSize, @@ -47,16 +51,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.DestinationSize = destinationSize; int maxWidth = (radius * 2) + 1; this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean); + this.pinHandle = this.data.Memory.Pin(); this.kernels = new ResizeKernel[destinationSize]; } public int DestinationSize { get; } /// - /// Disposes instance releasing it's backing buffer. + /// Disposes instance releasing it's backing buffer. /// public void Dispose() { + this.pinHandle.Dispose(); this.data.Dispose(); } @@ -73,8 +79,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The destination size /// The source size /// The to use for buffer allocations - /// The - public static KernelMap Calculate( + /// The + public static ResizeKernelMap Calculate( IResampler sampler, int destinationSize, int sourceSize, @@ -88,25 +94,40 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms scale = 1F; } - int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; int radius = (int)MathF.Ceiling(scale * sampler.Radius); - - var result = new KernelMap( - memoryAllocator, - sampler, - sourceSize, - destinationSize, - destinationSize, - ratio, - scale, - radius); - - result.BasicInit(); + int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; + float center0 = (ratio - 1) * 0.5f; + int cornerInterval = (int)MathF.Ceiling((radius - center0 - 1) / ratio); + + bool useMosaic = 2 * (cornerInterval + period) < destinationSize; + + ResizeKernelMap result = useMosaic + ? new ResizeKernelMap( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + destinationSize, + ratio, + scale, + radius) + : new MosaicKernelMap( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + ratio, + scale, + radius, + period, + cornerInterval); + + result.Init(); return result; } - private void BasicInit() + protected virtual void Init() { for (int i = 0; i < this.DestinationSize; i++) { @@ -157,7 +178,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Slices a weights value at the given positions. /// - private ResizeKernel CreateKernel(int destIdx, int left, int rightIdx) + private unsafe ResizeKernel CreateKernel(int destIdx, int left, int rightIdx) { int length = rightIdx - left + 1; @@ -166,10 +187,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms throw new InvalidOperationException($"Error in KernelMap.CreateKernel({destIdx},{left},{rightIdx}): left > this.data.Width"); } - int flatStartIndex = destIdx * this.data.Width; + Span rowSpan = this.data.GetRowSpan(destIdx); - Memory bufferSlice = this.data.Memory.Slice(flatStartIndex, length); - return new ResizeKernel(left, bufferSlice); + ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); + float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); + return new ResizeKernel(left, rowPtr, length); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs similarity index 98% rename from src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 7c9d39fc5..189e21de7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms where TPixel : struct, IPixel { // The following fields are not immutable but are optionally created on demand. - private KernelMap horizontalKernelMap; - private KernelMap verticalKernelMap; + private ResizeKernelMap horizontalKernelMap; + private ResizeKernelMap verticalKernelMap; /// /// Initializes a new instance of the class. @@ -165,13 +165,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // Since all image frame dimensions have to be the same we can calculate this for all frames. MemoryAllocator memoryAllocator = source.GetMemoryAllocator(); - this.horizontalKernelMap = KernelMap.Calculate( + this.horizontalKernelMap = ResizeKernelMap.Calculate( this.Sampler, this.ResizeRectangle.Width, sourceRectangle.Width, memoryAllocator); - this.verticalKernelMap = KernelMap.Calculate( + this.verticalKernelMap = ResizeKernelMap.Calculate( this.Sampler, this.ResizeRectangle.Height, sourceRectangle.Height, diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 3786ec6e3..85a930fb9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public partial class KernelMapTests { /// - /// Simplified reference implementation for functionality. + /// Simplified reference implementation for functionality. /// internal class ReferenceKernelMap { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 69de5dbbf..4d005576c 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms IResampler resampler = TestUtils.GetResampler(resamplerName); var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); - var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); + var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); #if DEBUG this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); @@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } - private static string PrintKernelMap(KernelMap kernelMap) => + private static string PrintKernelMap(ResizeKernelMap kernelMap) => PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); private static string PrintKernelMap(ReferenceKernelMap kernelMap) => diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 42cb083f1..839d26e71 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -58,6 +58,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + // TODO: Merge with the previous theory + add test images [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 1)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 10)] From e2279a04aa1e5c806b79f31c02bf27b4f83647f1 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 01:12:17 +0100 Subject: [PATCH 51/66] preparations for implementing MosaicKernelMap --- .../Transforms/Resize/ResizeKernel.cs | 12 +- .../Resize/ResizeKernelMap.MosaicKernelMap.cs | 5 + .../Transforms/Resize/ResizeKernelMap.cs | 131 ++++++++++-------- .../KernelMapTests.ReferenceKernelMap.cs | 2 +- .../Processors/Transforms/KernelMapTests.cs | 2 +- 5 files changed, 86 insertions(+), 66 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 1183de754..b2d7d2116 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -41,9 +41,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Gets the span representing the portion of the that this window covers /// - /// The - [MethodImpl(InliningOptions.ShortMethod)] - public Span GetValues() => new Span(this.bufferPtr, this.Length); + /// The + /// + public Span Values + { + [MethodImpl(InliningOptions.ShortMethod)] + get => new Span(this.bufferPtr, this.Length); + } /// /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. @@ -53,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public Vector4 Convolve(Span rowSpan) { - ref float horizontalValues = ref MemoryMarshal.GetReference(this.GetValues()); + ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); int left = this.Left; ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs index b815d05cb..09ae79677 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs @@ -41,6 +41,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.cornerInterval = cornerInterval; this.period = period; } + + protected override void Initialize() + { + base.Initialize(); + } } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 443db72d9..ebb15d376 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } /// - /// Returns a for an index value between 0 and destinationSize - 1. + /// Returns a for an index value between 0 and DestinationSize - 1. /// [MethodImpl(InliningOptions.ShortMethod)] public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx]; @@ -101,93 +101,104 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms bool useMosaic = 2 * (cornerInterval + period) < destinationSize; + useMosaic = false; + ResizeKernelMap result = useMosaic - ? new ResizeKernelMap( - memoryAllocator, - sampler, - sourceSize, - destinationSize, - destinationSize, - ratio, - scale, - radius) - : new MosaicKernelMap( - memoryAllocator, - sampler, - sourceSize, - destinationSize, - ratio, - scale, - radius, - period, - cornerInterval); - - result.Init(); + ? new MosaicKernelMap( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + ratio, + scale, + radius, + period, + cornerInterval) + : new ResizeKernelMap( + memoryAllocator, + sampler, + sourceSize, + destinationSize, + destinationSize, + ratio, + scale, + radius); + + result.Initialize(); return result; } - protected virtual void Init() + protected virtual void Initialize() { - for (int i = 0; i < this.DestinationSize; i++) + for (int destRowIndex = 0; destRowIndex < this.DestinationSize; destRowIndex++) { - float center = ((i + .5F) * this.ratio) - .5F; + ResizeKernel kernel = this.BuildKernelRow(destRowIndex, destRowIndex); + this.kernels[destRowIndex] = kernel; + } + } - // Keep inside bounds. - int left = (int)MathF.Ceiling(center - this.radius); - if (left < 0) - { - left = 0; - } + private ResizeKernel BuildKernelRow(int destRowIndex, int dataRowIndex) + { + float center = ((destRowIndex + .5F) * this.ratio) - .5F; - int right = (int)MathF.Floor(center + this.radius); - if (right > this.sourceSize - 1) - { - right = this.sourceSize - 1; - } + // Keep inside bounds. + int left = (int)MathF.Ceiling(center - this.radius); + if (left < 0) + { + left = 0; + } - float sum = 0; + int right = (int)MathF.Floor(center + this.radius); + if (right > this.sourceSize - 1) + { + right = this.sourceSize - 1; + } - ResizeKernel kernel = this.CreateKernel(i, left, right); - this.kernels[i] = kernel; + float sum = 0; - ref float kernelBaseRef = ref MemoryMarshal.GetReference(kernel.GetValues()); + ResizeKernel kernel = this.GetKernel(dataRowIndex, left, right); - for (int j = left; j <= right; j++) - { - float value = this.sampler.GetValue((j - center) / this.scale); - sum += value; + ref float kernelBaseRef = ref MemoryMarshal.GetReference(kernel.Values); - // weights[j - left] = weight: - Unsafe.Add(ref kernelBaseRef, j - left) = value; - } + for (int j = left; j <= right; j++) + { + float value = this.sampler.GetValue((j - center) / this.scale); + sum += value; - // Normalize, best to do it here rather than in the pixel loop later on. - if (sum > 0) + // weights[j - left] = weight: + Unsafe.Add(ref kernelBaseRef, j - left) = value; + } + + // Normalize, best to do it here rather than in the pixel loop later on. + if (sum > 0) + { + for (int w = 0; w < kernel.Length; w++) { - for (int w = 0; w < kernel.Length; w++) - { - // weights[w] = weights[w] / sum: - ref float kRef = ref Unsafe.Add(ref kernelBaseRef, w); - kRef /= sum; - } + // weights[w] = weights[w] / sum: + ref float kRef = ref Unsafe.Add(ref kernelBaseRef, w); + kRef /= sum; } } + + return kernel; } /// - /// Slices a weights value at the given positions. + /// Returns a referencing values of + /// at row . /// - private unsafe ResizeKernel CreateKernel(int destIdx, int left, int rightIdx) + private unsafe ResizeKernel GetKernel(int dataRowIndex, int left, int right) { - int length = rightIdx - left + 1; + int length = right - left + 1; if (length > this.data.Width) { - throw new InvalidOperationException($"Error in KernelMap.CreateKernel({destIdx},{left},{rightIdx}): left > this.data.Width"); + throw new InvalidOperationException( + $"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width"); } - Span rowSpan = this.data.GetRowSpan(destIdx); + Span rowSpan = this.data.GetRowSpan(dataRowIndex); ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 85a930fb9..f7c3b27e5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public static implicit operator ReferenceKernel(ResizeKernel orig) { - return new ReferenceKernel(orig.Left, orig.GetValues().ToArray()); + return new ReferenceKernel(orig.Left, orig.Values.ToArray()); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 4d005576c..7b997e33f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.Equal(referenceKernel.Length, kernel.Length); Assert.Equal(referenceKernel.Left, kernel.Left); float[] expectedValues = referenceKernel.Values; - Span actualValues = kernel.GetValues(); + Span actualValues = kernel.Values; Assert.Equal(expectedValues.Length, actualValues.Length); From b33b5687d124ed1bf53368fa6f3403d4bbc6d7c8 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 02:16:01 +0100 Subject: [PATCH 52/66] MosaicKernelMap works! --- .../Transforms/Resize/ResizeKernel.cs | 9 ++++-- .../Resize/ResizeKernelMap.MosaicKernelMap.cs | 32 ++++++++++++++++++- .../Transforms/Resize/ResizeKernelMap.cs | 26 +++++++++------ .../Processors/Transforms/KernelMapTests.cs | 16 ++++++++-- 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index b2d7d2116..e10afce2e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -6,14 +6,12 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; - namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// /// Points to a collection of of weights allocated in . /// - internal unsafe struct ResizeKernel + internal readonly unsafe struct ResizeKernel { private readonly float* bufferPtr; @@ -73,5 +71,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return result; } + + internal ResizeKernel AlterLeftValue(int left) + { + return new ResizeKernel(left, this.bufferPtr, this.Length); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs index 09ae79677..cf660a72f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs @@ -1,5 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. + +using System; + using SixLabors.Memory; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -42,9 +45,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.period = period; } + internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; + protected override void Initialize() { - base.Initialize(); + // Build top corner data + one period of the mosaic data: + int startOfFirstRepeatedMosaic = this.cornerInterval + this.period; + + for (int i = 0; i < startOfFirstRepeatedMosaic; i++) + { + ResizeKernel kernel = this.BuildKernel(i, i); + this.kernels[i] = kernel; + } + + // Copy the mosaics: + int bottomStartDest = this.DestinationSize - this.cornerInterval; + for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) + { + float center = ((i + .5F) * this.ratio) - .5F; + int left = (int)MathF.Ceiling(center - this.radius); + ResizeKernel kernel = this.kernels[i - this.period]; + this.kernels[i] = kernel.AlterLeftValue(left); + } + + // Build bottom corner data: + int bottomStartData = this.cornerInterval + this.period; + for (int i = 0; i < this.cornerInterval; i++) + { + ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i); + this.kernels[bottomStartDest + i] = kernel; + } } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index ebb15d376..262a39352 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -57,6 +57,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public int DestinationSize { get; } + internal virtual string Info => + $"radius:{this.radius}|sourceSize:{this.sourceSize}|destinationSize:{this.DestinationSize}|ratio:{this.ratio}|scale:{this.scale}"; + /// /// Disposes instance releasing it's backing buffer. /// @@ -97,11 +100,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int radius = (int)MathF.Ceiling(scale * sampler.Radius); int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; float center0 = (ratio - 1) * 0.5f; - int cornerInterval = (int)MathF.Ceiling((radius - center0 - 1) / ratio); + float firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; + int cornerInterval = (int)MathF.Ceiling(firstNonNegativeLeftVal); - bool useMosaic = 2 * (cornerInterval + period) < destinationSize; + // corner case for cornerInteval: + if (firstNonNegativeLeftVal == cornerInterval) + { + cornerInterval++; + } - useMosaic = false; + bool useMosaic = 2 * (cornerInterval + period) < destinationSize; ResizeKernelMap result = useMosaic ? new MosaicKernelMap( @@ -131,14 +139,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms protected virtual void Initialize() { - for (int destRowIndex = 0; destRowIndex < this.DestinationSize; destRowIndex++) + for (int i = 0; i < this.DestinationSize; i++) { - ResizeKernel kernel = this.BuildKernelRow(destRowIndex, destRowIndex); - this.kernels[destRowIndex] = kernel; + ResizeKernel kernel = this.BuildKernel(i, i); + this.kernels[i] = kernel; } } - private ResizeKernel BuildKernelRow(int destRowIndex, int dataRowIndex) + private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex) { float center = ((destRowIndex + .5F) * this.ratio) - .5F; @@ -157,7 +165,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float sum = 0; - ResizeKernel kernel = this.GetKernel(dataRowIndex, left, right); + ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); ref float kernelBaseRef = ref MemoryMarshal.GetReference(kernel.Values); @@ -188,7 +196,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Returns a referencing values of /// at row . /// - private unsafe ResizeKernel GetKernel(int dataRowIndex, int left, int right) + private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right) { int length = right - left + 1; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 7b997e33f..f0a0d738b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -48,6 +48,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos3), 5, 25 }, { nameof(KnownResamplers.Lanczos3), 5, 50 }, + { nameof(KnownResamplers.Lanczos3), 25, 5 }, + { nameof(KnownResamplers.Lanczos3), 50, 5 }, + { nameof(KnownResamplers.Lanczos3), 49, 5 }, + { nameof(KnownResamplers.Lanczos3), 31, 5 }, + { nameof(KnownResamplers.Lanczos8), 500, 200 }, { nameof(KnownResamplers.Lanczos8), 100, 10 }, { nameof(KnownResamplers.Lanczos8), 100, 80 }, @@ -75,8 +80,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); #if DEBUG + this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); - // this.Output.WriteLine($"Reference KernelMap:\n{PrintKernelMap(referenceMap)}\n"); #endif for (int i = 0; i < kernelMap.DestinationSize; i++) @@ -92,7 +97,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.Equal(expectedValues.Length, actualValues.Length); - var comparer = new ApproximateFloatComparer(1e-6f); + var comparer = new ApproximateFloatComparer(1e-4f); for (int x = 0; x < expectedValues.Length; x++) { @@ -116,12 +121,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { var bld = new StringBuilder(); + if (kernelMap is ResizeKernelMap actualMap) + { + bld.AppendLine(actualMap.Info); + } + int destinationSize = getDestinationSize(kernelMap); for (int i = 0; i < destinationSize; i++) { ReferenceKernel kernel = getKernel(kernelMap, i); - bld.Append($"({kernel.Left:D3}) || "); + bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || "); Span span = kernel.Values; for (int j = 0; j < kernel.Length; j++) From 2d96beed9acb84a061e96cf824274cb4356a240e Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 02:24:35 +0100 Subject: [PATCH 53/66] format, docs, cleanup --- .../Transforms/Resize/ResizeKernel.cs | 4 ++ .../Resize/ResizeKernelMap.MosaicKernelMap.cs | 10 ++--- .../Transforms/Resize/ResizeKernelMap.cs | 37 ++++++++++++------- .../Processors/Transforms/KernelMapTests.cs | 4 +- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index e10afce2e..04bf6c3a4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -72,6 +72,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return result; } + /// + /// Copy the contents of altering + /// to the value . + /// internal ResizeKernel AlterLeftValue(int left) { return new ResizeKernel(left, this.bufferPtr, this.Length); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs index cf660a72f..e106b1ea0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public MosaicKernelMap( MemoryAllocator memoryAllocator, IResampler sampler, - int sourceSize, - int destinationSize, + int sourceLength, + int destinationLength, float ratio, float scale, int radius, @@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms : base( memoryAllocator, sampler, - sourceSize, - destinationSize, + sourceLength, + destinationLength, (cornerInterval * 2) + period, ratio, scale, @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } // Copy the mosaics: - int bottomStartDest = this.DestinationSize - this.cornerInterval; + int bottomStartDest = this.DestinationLength - this.cornerInterval; for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) { float center = ((i + .5F) * this.ratio) - .5F; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 262a39352..ccb57114a 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -13,13 +13,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// /// Provides values from an optimized, - /// contigous memory region. + /// contiguous memory region. /// internal partial class ResizeKernelMap : IDisposable { private readonly IResampler sampler; - private readonly int sourceSize; + private readonly int sourceLength; private readonly float ratio; @@ -36,8 +36,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private ResizeKernelMap( MemoryAllocator memoryAllocator, IResampler sampler, - int sourceSize, - int destinationSize, + int sourceLength, + int destinationLength, int bufferHeight, float ratio, float scale, @@ -47,18 +47,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.ratio = ratio; this.scale = scale; this.radius = radius; - this.sourceSize = sourceSize; - this.DestinationSize = destinationSize; + this.sourceLength = sourceLength; + this.DestinationLength = destinationLength; int maxWidth = (radius * 2) + 1; this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean); this.pinHandle = this.data.Memory.Pin(); - this.kernels = new ResizeKernel[destinationSize]; + this.kernels = new ResizeKernel[destinationLength]; } - public int DestinationSize { get; } + /// + /// Gets the length of the destination row/column + /// + public int DestinationLength { get; } + /// + /// Gets a string of information to help debugging + /// internal virtual string Info => - $"radius:{this.radius}|sourceSize:{this.sourceSize}|destinationSize:{this.DestinationSize}|ratio:{this.ratio}|scale:{this.scale}"; + $"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}"; /// /// Disposes instance releasing it's backing buffer. @@ -104,7 +110,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int cornerInterval = (int)MathF.Ceiling(firstNonNegativeLeftVal); // corner case for cornerInteval: - if (firstNonNegativeLeftVal == cornerInterval) + if (firstNonNegativeLeftVal == cornerInterval) { cornerInterval++; } @@ -139,13 +145,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms protected virtual void Initialize() { - for (int i = 0; i < this.DestinationSize; i++) + for (int i = 0; i < this.DestinationLength; i++) { ResizeKernel kernel = this.BuildKernel(i, i); this.kernels[i] = kernel; } } + /// + /// Builds a for the row (in ) + /// referencing the data at row within , + /// so the data reusable by other data rows. + /// private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex) { float center = ((destRowIndex + .5F) * this.ratio) - .5F; @@ -158,9 +169,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } int right = (int)MathF.Floor(center + this.radius); - if (right > this.sourceSize - 1) + if (right > this.sourceLength - 1) { - right = this.sourceSize - 1; + right = this.sourceLength - 1; } float sum = 0; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index f0a0d738b..af98b9952 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif - for (int i = 0; i < kernelMap.DestinationSize; i++) + for (int i = 0; i < kernelMap.DestinationLength; i++) { ResizeKernel kernel = kernelMap.GetKernel(i); @@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } private static string PrintKernelMap(ResizeKernelMap kernelMap) => - PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); + PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i)); private static string PrintKernelMap(ReferenceKernelMap kernelMap) => PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i)); From 8cdb72888db1200b5d8dc333989a8150dd38bb11 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Nov 2018 14:21:53 +0000 Subject: [PATCH 54/66] Add Skew methods and tests. --- .../Processing/AffineTransformBuilder.cs | 58 ++++++++++++ .../Processors/Transforms/TransformUtils.cs | 12 +++ .../Processing/ProjectiveTransformBuilder.cs | 76 +++++++++++++++ .../Transforms/AffineTransformBuilderTests.cs | 51 +++++++--- .../ProjectiveTransformBuilderTests.cs | 30 ++++-- .../Transforms/TransformBuilderTestBase.cs | 94 ++++++++++++++++--- 6 files changed, 291 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index ef0b24f81..c3d01241c 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -145,6 +145,35 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) => this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + /// /// Appends a centered skew matrix from the give angles in degrees. /// @@ -154,6 +183,35 @@ namespace SixLabors.ImageSharp.Processing public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) => this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size)); + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin)); + /// /// Prepends a translation matrix from the given vector. /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 6cef38f8f..24b15d309 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -46,6 +46,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms new Rectangle(Point.Empty, size), Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); + /// + /// Creates a centered skew matrix from the give angles in radians and the source size. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The source image size. + /// The . + public static Matrix3x2 CreateSkewMatrixRadians(float radiansX, float radiansY, Size size) + => CreateCenteredTransformMatrix( + new Rectangle(Point.Empty, size), + Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); + /// /// Gets the centered transform matrix based upon the source and destination rectangles. /// diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 5a19b890d..c29941d07 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -152,6 +152,82 @@ namespace SixLabors.ImageSharp.Processing public ProjectiveTransformBuilder AppendScale(Vector2 scales) => this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F))); + /// + /// Prepends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Prepends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); + + /// + /// Prepends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Prepends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.PrependMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + + /// + /// Appends a centered skew matrix from the give angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The . + internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Appends a centered skew matrix from the give angles in radians. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size))); + + /// + /// Appends a skew matrix using the given angles in degrees at the given origin. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin) + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin); + + /// + /// Appends a skew matrix using the given angles in radians at the given origin. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The skew origin point. + /// The . + public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin) + => this.AppendMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin))); + /// /// Prepends a translation matrix from the given vector. /// diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 14b867264..70159e18a 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -9,28 +9,55 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { public class AffineTransformBuilderTests : TransformBuilderTestBase { - protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); - protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) + => builder.AppendRotationDegrees(degrees); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); + protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) + => builder.AppendRotationDegrees(degrees, origin); - protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) + => builder.AppendRotationRadians(radians); - protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) + => builder.AppendRotationRadians(radians, origin); - protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); + protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) + => builder.AppendScale(scale); - protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY) + => builder.AppendSkewDegrees(degreesX, degreesY); - protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) + => builder.AppendSkewDegrees(degreesX, degreesY, origin); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) + => builder.AppendSkewRadians(radiansX, radiansY); - protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) => - builder.PrependRotationRadians(radians, origin); + protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.AppendSkewRadians(radiansX, radiansY, origin); - protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder(); + protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) + => builder.AppendTranslation(translate); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) + => builder.PrependRotationRadians(radians); + + protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin) + => builder.PrependRotationRadians(radians, origin); + + protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) + => builder.PrependScale(scale); + + protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY) + => builder.PrependSkewRadians(radiansX, radiansY); + + protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.PrependSkewRadians(radiansX, radiansY, origin); + + protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) + => builder.PrependTranslation(translate); protected override Vector2 Execute( AffineTransformBuilder builder, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index 9f58826cd..d82cd1689 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -11,24 +11,42 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms { protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(); - protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); - protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); + protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians); protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin); - protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees); + protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale); - protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin); + protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY) + => builder.AppendSkewDegrees(degreesX, degreesY); - protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY, Vector2 origin) + => builder.AppendSkewDegrees(degreesX, degreesY, origin); - protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) + => builder.AppendSkewRadians(radiansX, radiansY); + + protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.AppendSkewRadians(radiansX, radiansY, origin); + + protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate); protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians); + protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale); + + protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY) + => builder.PrependSkewRadians(radiansX, radiansY); + + protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin) + => builder.PrependSkewRadians(radiansX, radiansY, origin); + + protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate); + protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.PrependRotationRadians(radians, origin); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 43ad756e0..71e3b7179 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -86,17 +86,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter( int width, int height, - float deg, + float degrees, float x, float y) { var size = new Size(width, height); TBuilder builder = this.CreateBuilder(size); - this.AppendRotationDegrees(builder, deg); + this.AppendRotationDegrees(builder, degrees); // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness - Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size); + Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(degrees, size); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -112,7 +112,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public void AppendRotationDegrees_WithRotationCenter( int width, int height, - float deg, + float degrees, float cx, float cy, float x, @@ -122,9 +122,63 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms TBuilder builder = this.CreateBuilder(size); var centerPoint = new Vector2(cx, cy); - this.AppendRotationDegrees(builder, deg, centerPoint); + this.AppendRotationDegrees(builder, degrees, centerPoint); - var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(deg), centerPoint); + var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 10, 42, 84)] + [InlineData(200, 100, 100, 100, 42, 84)] + [InlineData(100, 200, -10, -10, 42, 84)] + public void AppendSkewDegrees_WithoutSpecificSkewCenter_SkewIsCenteredAroundImageCenter( + int width, + int height, + float degreesX, + float degreesY, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + this.AppendSkewDegrees(builder, degreesX, degreesY); + + Matrix3x2 matrix = TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size); + + var position = new Vector2(x, y); + var expected = Vector2.Transform(position, matrix); + Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position); + Assert.Equal(actual, expected, Comparer); + } + + [Theory] + [InlineData(200, 100, 10, 10, 30, 61, 42, 84)] + [InlineData(200, 100, 100, 100, 30, 10, 20, 84)] + [InlineData(100, 200, -10, -10, 30, 20, 11, 84)] + public void AppendSkewDegrees_WithSkewCenter( + int width, + int height, + float degreesX, + float degreesY, + float cx, + float cy, + float x, + float y) + { + var size = new Size(width, height); + TBuilder builder = this.CreateBuilder(size); + + var centerPoint = new Vector2(cx, cy); + this.AppendSkewDegrees(builder, degreesX, degreesY, centerPoint); + + var matrix = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -144,14 +198,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms // Forwards this.AppendRotationRadians(b1, pi); + this.AppendSkewRadians(b1, pi, pi); this.AppendScale(b1, new SizeF(2, 0.5f)); this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f)); + this.AppendSkewRadians(b1, pi, pi / 2, new Vector2(-0.5f, -0.1f)); this.AppendTranslation(b1, new PointF(123, 321)); // Backwards this.PrependTranslation(b2, new PointF(123, 321)); + this.PrependSkewRadians(b2, pi, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f)); this.PrependScale(b2, new SizeF(2, 0.5f)); + this.PrependSkewRadians(b2, pi, pi); this.PrependRotationRadians(b2, pi); Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65)); @@ -180,25 +238,37 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms protected abstract TBuilder CreateBuilder(Rectangle rectangle); - protected abstract void AppendTranslation(TBuilder builder, PointF translate); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); - protected abstract void AppendScale(TBuilder builder, SizeF scale); + protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); protected abstract void AppendRotationRadians(TBuilder builder, float radians); protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void AppendScale(TBuilder builder, SizeF scale); - protected abstract void PrependScale(TBuilder builder, SizeF scale); + protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY); + + protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY, Vector2 origin); + + protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY); + + protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); + + protected abstract void AppendTranslation(TBuilder builder, PointF translate); protected abstract void PrependRotationRadians(TBuilder builder, float radians); protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin); - protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); + protected abstract void PrependScale(TBuilder builder, SizeF scale); - protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin); + protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY); + + protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin); + + protected abstract void PrependTranslation(TBuilder builder, PointF translate); protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); } From 2742e1923c369bc880d9649aa7fa3c1ebb3a92d0 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 18:44:28 +0100 Subject: [PATCH 55/66] cleanup & undo WIP project setup --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 3 +-- src/ImageSharp/ImageSharp.csproj | 3 +-- ...KernelMap.cs => ResizeKernelMap.PeriodicKernelMap.cs} | 6 +++--- .../Processors/Transforms/Resize/ResizeKernelMap.cs | 2 +- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 3 +-- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 3 +-- .../Transforms/KernelMapTests.ReferenceKernelMap.cs | 6 +++--- .../Processing/Processors/Transforms/KernelMapTests.cs | 9 ++++----- 8 files changed, 15 insertions(+), 20 deletions(-) rename src/ImageSharp/Processing/Processors/Transforms/Resize/{ResizeKernelMap.MosaicKernelMap.cs => ResizeKernelMap.PeriodicKernelMap.cs} (94%) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 6341e1771..1cb3f444f 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -5,8 +5,7 @@ $(packageversion) 0.0.1 SixLabors and contributors - - netcoreapp2.1 + netstandard1.3;netstandard2.0 7.3 true true diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index e2f55e3c6..29d29d50d 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -5,8 +5,7 @@ $(packageversion) 0.0.1 Six Labors and contributors - - netcoreapp2.1 + netstandard1.3;netstandard2.0;netcoreapp2.1;net472 true true SixLabors.ImageSharp diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs similarity index 94% rename from src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs index e106b1ea0..b7b581c18 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.MosaicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -8,20 +8,20 @@ using SixLabors.Memory; namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Contains + /// Contains /// internal partial class ResizeKernelMap { /// /// Memory-optimized where repeating rows are stored only once. /// - private sealed class MosaicKernelMap : ResizeKernelMap + private sealed class PeriodicKernelMap : ResizeKernelMap { private readonly int period; private readonly int cornerInterval; - public MosaicKernelMap( + public PeriodicKernelMap( MemoryAllocator memoryAllocator, IResampler sampler, int sourceLength, diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index ccb57114a..011a4ffa2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms bool useMosaic = 2 * (cornerInterval + period) < destinationSize; ResizeKernelMap result = useMosaic - ? new MosaicKernelMap( + ? new PeriodicKernelMap( memoryAllocator, sampler, sourceSize, diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 04a4541b2..a705c9bac 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -1,7 +1,6 @@  - - netcoreapp2.1 + netcoreapp2.1;net461 Exe True SixLabors.ImageSharp.Benchmarks diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 75ac7450c..86c1a7a25 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -1,7 +1,6 @@  - - netcoreapp2.1 + net462;net472;netcoreapp2.1 True latest full diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index f7c3b27e5..12c701609 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms scale = 1F; } - float radius = MathF.Ceiling(scale * sampler.Radius); + float radius = (float)Math.Ceiling(scale * sampler.Radius); var result = new List(); @@ -42,13 +42,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms float center = ((i + .5F) * ratio) - .5F; // Keep inside bounds. - int left = (int)MathF.Ceiling(center - radius); + int left = (int)Math.Ceiling(center - radius); if (left < 0) { left = 0; } - int right = (int)MathF.Floor(center + radius); + int right = (int)Math.Floor(center + radius); if (right > sourceSize - 1) { right = sourceSize - 1; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index af98b9952..962c599e6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -1,12 +1,11 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; using System.Text; -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; -using SixLabors.Primitives; using Xunit; using Xunit.Abstractions; From 03324e3c218ee97ce13f2d7cecf9dd4d2ef9cf58 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 23:20:57 +0100 Subject: [PATCH 56/66] additional extensive testing --- .../KernelMapTests.ReferenceKernelMap.cs | 21 +++--- .../Processors/Transforms/KernelMapTests.cs | 70 +++++++++++++++++-- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 12c701609..9a7052b5a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -25,21 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true) { - float ratio = (float)sourceSize / destinationSize; - float scale = ratio; + double ratio = (double)sourceSize / destinationSize; + double scale = ratio; if (scale < 1F) { scale = 1F; } - float radius = (float)Math.Ceiling(scale * sampler.Radius); + double radius = (double)Math.Ceiling(scale * sampler.Radius); var result = new List(); for (int i = 0; i < destinationSize; i++) { - float center = ((i + .5F) * ratio) - .5F; + double center = ((i + .5) * ratio) - .5; // Keep inside bounds. int left = (int)Math.Ceiling(center - radius); @@ -54,20 +55,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms right = sourceSize - 1; } - float sum = 0; + double sum = 0; - float[] values = new float[right - left + 1]; + double[] values = new double[right - left + 1]; for (int j = left; j <= right; j++) { - float weight = sampler.GetValue((j - center) / scale); + double weight = sampler.GetValue((float)((j - center) / scale)); sum += weight; values[j - left] = weight; } - result.Add(new ReferenceKernel(left, values)); - if (sum > 0 && normalize) { for (int w = 0; w < values.Length; w++) @@ -75,6 +74,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms values[w] /= sum; } } + + float[] floatVals = values.Select(v => (float)v).ToArray(); + + result.Add(new ReferenceKernel(left, floatVals)); } return new ReferenceKernelMap(result.ToArray()); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index 962c599e6..dfbd7a28b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -2,6 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Text; using SixLabors.ImageSharp.Processing; @@ -58,6 +61,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos8), 10, 100 }, }; + public static TheoryData GeneratedImageResizeData = + GenerateImageResizeData(); + + [Theory] [MemberData(nameof(KernelMapData))] public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) @@ -71,7 +78,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [MemberData(nameof(KernelMapData))] + //[MemberData(nameof(GeneratedImageResizeData))] public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) + { + VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); + } + + [Theory] + [MemberData(nameof(GeneratedImageResizeData))] + public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) + { + VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); + } + + + private static void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { IResampler resampler = TestUtils.GetResampler(resamplerName); @@ -79,8 +100,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); #if DEBUG - this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); - this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); + // this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); + // this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif for (int i = 0; i < kernelMap.DestinationLength; i++) @@ -89,14 +110,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms ReferenceKernel referenceKernel = referenceMap.GetKernel(i); - Assert.Equal(referenceKernel.Length, kernel.Length); - Assert.Equal(referenceKernel.Left, kernel.Left); + Assert.True( + referenceKernel.Length == kernel.Length, + $"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}"); + Assert.True( + referenceKernel.Left == kernel.Left, + $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}"); float[] expectedValues = referenceKernel.Values; Span actualValues = kernel.Values; - + Assert.Equal(expectedValues.Length, actualValues.Length); - var comparer = new ApproximateFloatComparer(1e-4f); + var comparer = new ApproximateFloatComparer(1e-6f); for (int x = 0; x < expectedValues.Length; x++) { @@ -145,5 +170,38 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms return bld.ToString(); } + + + private static TheoryData GenerateImageResizeData() + { + var result = new TheoryData(); + + string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static) + .Select(p => p.Name).ToArray(); + + int[] dimensionVals = + { + // Arbitrary, small dimensions: + 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, + + // Typical image sizes: + 640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, + 1920, 3032, 2008, 3072, 2304, 3264, 2448 + }; + + IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals + .SelectMany(s => dimensionVals.Select(d => (s, d))) + .OrderBy(x => x.s + x.d); + + foreach (string resampler in resamplerNames) + { + foreach ((int s, int d) x in source2Dest) + { + result.Add(resampler, x.s, x.d); + } + } + + return result; + } } } \ No newline at end of file From ee1f398b1e13b76eb3a244ddfaed0640b5a58526 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 28 Nov 2018 23:21:30 +0100 Subject: [PATCH 57/66] use double precision in KernelMap calculations --- .../Transforms/Resize/ResizeKernel.cs | 10 +++ .../ResizeKernelMap.PeriodicKernelMap.cs | 8 +-- .../Transforms/Resize/ResizeKernelMap.cs | 66 ++++++++++--------- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 04bf6c3a4..f349634ac 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -80,5 +80,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { return new ResizeKernel(left, this.bufferPtr, this.Length); } + + internal void Fill(Span values) + { + DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!"); + + for (int i = 0; i < this.Length; i++) + { + this.Values[i] = (float)values[i]; + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs index b7b581c18..e0f5ad261 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -26,8 +26,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms IResampler sampler, int sourceLength, int destinationLength, - float ratio, - float scale, + double ratio, + double scale, int radius, int period, int cornerInterval) @@ -62,8 +62,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int bottomStartDest = this.DestinationLength - this.cornerInterval; for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) { - float center = ((i + .5F) * this.ratio) - .5F; - int left = (int)MathF.Ceiling(center - this.radius); + double center = ((i + .5) * this.ratio) - .5; + int left = (int)Math.Ceiling(center - this.radius); ResizeKernel kernel = this.kernels[i - this.period]; this.kernels[i] = kernel.AlterLeftValue(left); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 011a4ffa2..4cd9928d3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -17,13 +17,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal partial class ResizeKernelMap : IDisposable { + private readonly MemoryAllocator memoryAllocator; + private readonly IResampler sampler; private readonly int sourceLength; - private readonly float ratio; + private readonly double ratio; - private readonly float scale; + private readonly double scale; private readonly int radius; @@ -39,10 +41,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int sourceLength, int destinationLength, int bufferHeight, - float ratio, - float scale, + double ratio, + double scale, int radius) { + this.memoryAllocator = memoryAllocator; this.sampler = sampler; this.ratio = ratio; this.scale = scale; @@ -95,19 +98,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int sourceSize, MemoryAllocator memoryAllocator) { - float ratio = (float)sourceSize / destinationSize; - float scale = ratio; + double ratio = (double)sourceSize / destinationSize; + double scale = ratio; if (scale < 1F) { scale = 1F; } - int radius = (int)MathF.Ceiling(scale * sampler.Radius); + int radius = (int)Math.Ceiling(scale * sampler.Radius); int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; - float center0 = (ratio - 1) * 0.5f; - float firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; - int cornerInterval = (int)MathF.Ceiling(firstNonNegativeLeftVal); + double center0 = (ratio - 1) * 0.5f; + double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; + int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal); // corner case for cornerInteval: if (firstNonNegativeLeftVal == cornerInterval) @@ -159,45 +162,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex) { - float center = ((destRowIndex + .5F) * this.ratio) - .5F; + double center = ((destRowIndex + .5) * this.ratio) - .5; // Keep inside bounds. - int left = (int)MathF.Ceiling(center - this.radius); + int left = (int)Math.Ceiling(center - this.radius); if (left < 0) { left = 0; } - int right = (int)MathF.Floor(center + this.radius); + int right = (int)Math.Floor(center + this.radius); if (right > this.sourceLength - 1) { right = this.sourceLength - 1; } - float sum = 0; - ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); - ref float kernelBaseRef = ref MemoryMarshal.GetReference(kernel.Values); - - for (int j = left; j <= right; j++) + using (IMemoryOwner tempBuffer = this.memoryAllocator.Allocate(kernel.Length)) { - float value = this.sampler.GetValue((j - center) / this.scale); - sum += value; + Span kernelValues = tempBuffer.GetSpan(); + double sum = 0; - // weights[j - left] = weight: - Unsafe.Add(ref kernelBaseRef, j - left) = value; - } + for (int j = left; j <= right; j++) + { + double value = this.sampler.GetValue((float)((j - center) / this.scale)); + sum += value; - // Normalize, best to do it here rather than in the pixel loop later on. - if (sum > 0) - { - for (int w = 0; w < kernel.Length; w++) + kernelValues[j - left] = value; + } + + // Normalize, best to do it here rather than in the pixel loop later on. + if (sum > 0) { - // weights[w] = weights[w] / sum: - ref float kRef = ref Unsafe.Add(ref kernelBaseRef, w); - kRef /= sum; + for (int j = 0; j < kernel.Length; j++) + { + // weights[w] = weights[w] / sum: + ref double kRef = ref kernelValues[j]; + kRef /= sum; + } } + + kernel.Fill(kernelValues); } return kernel; From 97357b45e461a67e6cbe7c437c86cab3c5574d56 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 01:05:07 +0100 Subject: [PATCH 58/66] cherry pick test cases from auto-generated set --- .../Processors/Transforms/KernelMapTests.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index dfbd7a28b..d0ff62a91 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -59,6 +59,30 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos8), 100, 10 }, { nameof(KnownResamplers.Lanczos8), 100, 80 }, { nameof(KnownResamplers.Lanczos8), 10, 100 }, + + // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData + { nameof(KnownResamplers.Box), 201, 100 }, + { nameof(KnownResamplers.Box), 199, 99 }, + { nameof(KnownResamplers.Box), 10, 299 }, + { nameof(KnownResamplers.Box), 299, 10 }, + { nameof(KnownResamplers.Box), 301, 300 }, + { nameof(KnownResamplers.Box), 1180, 480 }, + + { nameof(KnownResamplers.Lanczos2), 3264, 3032 }, + + { nameof(KnownResamplers.Bicubic), 1280, 2240 }, + { nameof(KnownResamplers.Bicubic), 1920, 1680 }, + { nameof(KnownResamplers.Bicubic), 3072, 2240 }, + + { nameof(KnownResamplers.Welch), 300, 2008 }, + + // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData + { nameof(KnownResamplers.Bicubic), 10, 50 }, + { nameof(KnownResamplers.Bicubic), 49, 301 }, + { nameof(KnownResamplers.Bicubic), 301, 49 }, + { nameof(KnownResamplers.Bicubic), 1680, 1200 }, + { nameof(KnownResamplers.Box), 13, 299 }, + { nameof(KnownResamplers.Lanczos5), 3032, 600 }, }; public static TheoryData GeneratedImageResizeData = @@ -78,18 +102,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [Theory] [MemberData(nameof(KernelMapData))] - //[MemberData(nameof(GeneratedImageResizeData))] public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); } + // Comprehensive but expensive tests, for KernelMap generation + // Enabling them can kill your IDE: +#if false [Theory] [MemberData(nameof(GeneratedImageResizeData))] public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) { VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); } +#endif private static void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) @@ -103,6 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); // this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif + var comparer = new ApproximateFloatComparer(1e-6f); for (int i = 0; i < kernelMap.DestinationLength; i++) { @@ -121,7 +149,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.Equal(expectedValues.Length, actualValues.Length); - var comparer = new ApproximateFloatComparer(1e-6f); + for (int x = 0; x < expectedValues.Length; x++) { @@ -177,7 +205,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var result = new TheoryData(); string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static) - .Select(p => p.Name).ToArray(); + .Select(p => p.Name) + .Where(name => name != nameof(KnownResamplers.NearestNeighbor)) + .ToArray(); int[] dimensionVals = { From 3189c91563f3a4cb805b68137e5f4dd11c4b98e5 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 02:34:49 +0100 Subject: [PATCH 59/66] introduce TolerantMath --- src/ImageSharp/Common/Helpers/TolerantMath.cs | 75 ++++++++++ .../Transforms/Resize/ResizeKernelMap.cs | 47 ++++--- .../Helpers/ImageMathsTests.cs | 3 +- .../Helpers/TolerantMathTests.cs | 130 ++++++++++++++++++ .../KernelMapTests.ReferenceKernelMap.cs | 6 + .../Processors/Transforms/KernelMapTests.cs | 15 +- 6 files changed, 243 insertions(+), 33 deletions(-) create mode 100644 src/ImageSharp/Common/Helpers/TolerantMath.cs create mode 100644 tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs new file mode 100644 index 000000000..b9b3b8ea1 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TolerantMath.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp +{ + /// + /// Implements math operations using tolerant comparison. + /// + internal struct TolerantMath + { + private readonly double epsilon; + + private readonly double negEpsilon; + + public TolerantMath(double epsilon) + { + DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon)); + + this.epsilon = epsilon; + this.negEpsilon = -epsilon; + } + + public static TolerantMath Default { get; } = new TolerantMath(1e-8); + + /// + /// == 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon; + + /// + /// > 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsPositive(double a) => a > this.epsilon; + + /// + /// < 0 + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsNegative(double a) => a < this.negEpsilon; + + /// + /// == + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool AreEqual(double a, double b) => this.IsZero(a - b); + + /// + /// > + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsGreater(double a, double b) => a > b + this.epsilon; + + /// + /// < + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsLess(double a, double b) => a < b - this.epsilon; + + /// + /// >= + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon; + + /// + /// <= + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 4cd9928d3..468e0d844 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -17,8 +17,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal partial class ResizeKernelMap : IDisposable { - private readonly MemoryAllocator memoryAllocator; - private readonly IResampler sampler; private readonly int sourceLength; @@ -35,6 +33,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ResizeKernel[] kernels; + // To avoid both GC allocations, and MemoryAllocator ceremony: + private readonly double[] tempValues; + private ResizeKernelMap( MemoryAllocator memoryAllocator, IResampler sampler, @@ -45,7 +46,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms double scale, int radius) { - this.memoryAllocator = memoryAllocator; this.sampler = sampler; this.ratio = ratio; this.scale = scale; @@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean); this.pinHandle = this.data.Memory.Pin(); this.kernels = new ResizeKernel[destinationLength]; + this.tempValues = new double[maxWidth]; } /// @@ -113,7 +114,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal); // corner case for cornerInteval: - if (firstNonNegativeLeftVal == cornerInterval) + // TODO: Implement library-wide utils for tolerant comparison + if (Math.Abs(firstNonNegativeLeftVal - cornerInterval) < 1e-8) { cornerInterval++; } @@ -179,33 +181,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); - using (IMemoryOwner tempBuffer = this.memoryAllocator.Allocate(kernel.Length)) - { - Span kernelValues = tempBuffer.GetSpan(); - double sum = 0; + Span kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length); + double sum = 0; - for (int j = left; j <= right; j++) - { - double value = this.sampler.GetValue((float)((j - center) / this.scale)); - sum += value; + for (int j = left; j <= right; j++) + { + double value = this.sampler.GetValue((float)((j - center) / this.scale)); + sum += value; - kernelValues[j - left] = value; - } + kernelValues[j - left] = value; + } - // Normalize, best to do it here rather than in the pixel loop later on. - if (sum > 0) + // Normalize, best to do it here rather than in the pixel loop later on. + if (sum > 0) + { + for (int j = 0; j < kernel.Length; j++) { - for (int j = 0; j < kernel.Length; j++) - { - // weights[w] = weights[w] / sum: - ref double kRef = ref kernelValues[j]; - kRef /= sum; - } + // weights[w] = weights[w] / sum: + ref double kRef = ref kernelValues[j]; + kRef /= sum; } - - kernel.Fill(kernelValues); } + kernel.Fill(kernelValues); + return kernel; } diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs index 75ef611a5..018fabd98 100644 --- a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs @@ -2,11 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; +using Xunit; namespace SixLabors.ImageSharp.Tests.Helpers { - using Xunit; - public class ImageMathsTests { [Theory] diff --git a/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs new file mode 100644 index 000000000..d488d6491 --- /dev/null +++ b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using Xunit; +// ReSharper disable InconsistentNaming + +namespace SixLabors.ImageSharp.Tests.Helpers +{ + public class TolerantMathTests + { + private readonly TolerantMath tolerantMath = new TolerantMath(0.1); + + [Theory] + [InlineData(0)] + [InlineData(0.01)] + [InlineData(-0.05)] + public void IsZero_WhenTrue(double a) + { + Assert.True(this.tolerantMath.IsZero(a)); + } + + [Theory] + [InlineData(0.11)] + [InlineData(-0.101)] + [InlineData(42)] + public void IsZero_WhenFalse(double a) + { + Assert.False(this.tolerantMath.IsZero(a)); + } + + [Theory] + [InlineData(0.11)] + [InlineData(100)] + public void IsPositive_WhenTrue(double a) + { + Assert.True(this.tolerantMath.IsPositive(a)); + } + + [Theory] + [InlineData(0.09)] + [InlineData(-0.1)] + [InlineData(-1000)] + public void IsPositive_WhenFalse(double a) + { + Assert.False(this.tolerantMath.IsPositive(a)); + } + + [Theory] + [InlineData(-0.11)] + [InlineData(-100)] + public void IsNegative_WhenTrue(double a) + { + Assert.True(this.tolerantMath.IsNegative(a)); + } + + [Theory] + [InlineData(-0.09)] + [InlineData(0.1)] + [InlineData(1000)] + public void IsNegative_WhenFalse(double a) + { + Assert.False(this.tolerantMath.IsNegative(a)); + } + + [Theory] + [InlineData(4.2, 4.2)] + [InlineData(4.2, 4.25)] + [InlineData(-Math.PI, -Math.PI + 0.05)] + [InlineData(999999.2, 999999.25)] + public void AreEqual_WhenTrue(double a, double b) + { + Assert.True(this.tolerantMath.AreEqual(a, b)); + } + + [Theory] + [InlineData(1, 2)] + [InlineData(-1000000, -1000000.2)] + public void AreEqual_WhenFalse(double a, double b) + { + Assert.False(this.tolerantMath.AreEqual(a, b)); + } + + [Theory] + [InlineData(2, 1.8)] + [InlineData(-20, -20.2)] + [InlineData(0.1, -0.1)] + [InlineData(100, 10)] + public void IsGreater_IsLess_WhenTrue(double a, double b) + { + Assert.True(this.tolerantMath.IsGreater(a, b)); + Assert.True(this.tolerantMath.IsLess(b, a)); + } + + [Theory] + [InlineData(2, 1.95)] + [InlineData(-20, -20.02)] + [InlineData(0.01, -0.01)] + [InlineData(999999, 999999.09)] + public void IsGreater_IsLess_WhenFalse(double a, double b) + { + Assert.False(this.tolerantMath.IsGreater(a, b)); + Assert.False(this.tolerantMath.IsLess(b, a)); + } + + [Theory] + [InlineData(3, 2)] + [InlineData(3, 2.99)] + [InlineData(2.99, 3)] + [InlineData(-5, -6)] + [InlineData(-5, -5.05)] + [InlineData(-5.05, -5)] + public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b) + { + Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b)); + Assert.True(this.tolerantMath.IsLessOrEqual(b, a)); + } + + [Theory] + [InlineData(2, 3)] + [InlineData(2.89, 3)] + [InlineData(-3, -2.89)] + public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b) + { + Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b)); + Assert.False(this.tolerantMath.IsLessOrEqual(b, a)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 9a7052b5a..31907b06d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -40,6 +41,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms for (int i = 0; i < destinationSize; i++) { + if (i == 21 || i == 64) + { + Debug.Print("lol"); + } + double center = ((i + .5) * ratio) - .5; // Keep inside bounds. diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index d0ff62a91..dc7a441e9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -104,22 +104,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms [MemberData(nameof(KernelMapData))] public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { - VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); + this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); } - // Comprehensive but expensive tests, for KernelMap generation - // Enabling them can kill your IDE: + // Comprehensive but expensive tests, for ResizeKernelMap. + // Enabling them can kill you, but sometimes you have to wear the burden! + // AppVeyor will never follow you to these shadows of Mordor. #if false [Theory] [MemberData(nameof(GeneratedImageResizeData))] public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) { - VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); + this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); } #endif - private static void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) + private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { IResampler resampler = TestUtils.GetResampler(resamplerName); @@ -127,8 +128,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); #if DEBUG - // this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); - // this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); + this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); + this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif var comparer = new ApproximateFloatComparer(1e-6f); From 49ef404552c15929c5d83a4e93b361df4325c11a Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 02:54:31 +0100 Subject: [PATCH 60/66] TolerantMath: implemented Floor and Ceiling --- src/ImageSharp/Common/Helpers/TolerantMath.cs | 28 +++++++++++++- .../Helpers/TolerantMathTests.cs | 38 +++++++++++++++++++ .../ImageSharp.Tests/ImageSharp.Tests.csproj | 3 +- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs index b9b3b8ea1..d95aea9de 100644 --- a/src/ImageSharp/Common/Helpers/TolerantMath.cs +++ b/src/ImageSharp/Common/Helpers/TolerantMath.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp { /// - /// Implements math operations using tolerant comparison. + /// Implements basic math operations using tolerant comparison + /// whenever an equality check is needed. /// internal struct TolerantMath { @@ -71,5 +73,29 @@ namespace SixLabors.ImageSharp /// [MethodImpl(InliningOptions.ShortMethod)] public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon; + + [MethodImpl(InliningOptions.ShortMethod)] + public double Ceiling(double a) + { + double rem = Math.IEEERemainder(a, 1); + if (this.IsZero(rem)) + { + return Math.Round(a); + } + + return Math.Ceiling(a); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public double Floor(double a) + { + double rem = Math.IEEERemainder(a, 1); + if (this.IsZero(rem)) + { + return Math.Round(a); + } + + return Math.Floor(a); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs index d488d6491..6c7a1f275 100644 --- a/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs +++ b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs @@ -126,5 +126,43 @@ namespace SixLabors.ImageSharp.Tests.Helpers Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b)); Assert.False(this.tolerantMath.IsLessOrEqual(b, a)); } + + [Theory] + [InlineData(3.5, 4.0)] + [InlineData(3.89, 4.0)] + [InlineData(4.09, 4.0)] + [InlineData(4.11, 5.0)] + [InlineData(0.11, 1)] + [InlineData(0.05, 0)] + [InlineData(-0.5, 0)] + [InlineData(-0.95, -1)] + [InlineData(-1.05, -1)] + [InlineData(-1.5, -1)] + public void Ceiling(double value, double expected) + { + double actual = this.tolerantMath.Ceiling(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 1)] + [InlineData(0.99, 1)] + [InlineData(0.5, 0)] + [InlineData(0.01, 0)] + [InlineData(-0.09, 0)] + [InlineData(-0.11, -1)] + [InlineData(-100.11, -101)] + [InlineData(-100.09, -100)] + public void Floor(double value, double expected) + { + double plz1 = Math.IEEERemainder(1.1, 1); + double plz2 = Math.IEEERemainder(0.9, 1); + + double plz3 = Math.IEEERemainder(-1.1, 1); + double plz4 = Math.IEEERemainder(-0.9, 1); + + double actual = this.tolerantMath.Floor(value); + Assert.Equal(expected, actual); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 86c1a7a25..75ac7450c 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -1,6 +1,7 @@  - net462;net472;netcoreapp2.1 + + netcoreapp2.1 True latest full From b4271cc20d17b148b9e312211b2b339a1d172b98 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 03:05:38 +0100 Subject: [PATCH 61/66] robust ResizeKernelMap calculations using TolerantMath --- .../Resize/ResizeKernelMap.PeriodicKernelMap.cs | 2 +- .../Transforms/Resize/ResizeKernelMap.cs | 15 ++++++++------- tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 3 +-- .../KernelMapTests.ReferenceKernelMap.cs | 16 +++++++++------- .../Processors/Transforms/KernelMapTests.cs | 1 - 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs index e0f5ad261..4b81aaa64 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) { double center = ((i + .5) * this.ratio) - .5; - int left = (int)Math.Ceiling(center - this.radius); + int left = (int)TolerantMath.Ceiling(center - this.radius); ResizeKernel kernel = this.kernels[i - this.period]; this.kernels[i] = kernel.AlterLeftValue(left); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 468e0d844..347aaf0be 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -17,6 +17,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal partial class ResizeKernelMap : IDisposable { + private static readonly TolerantMath TolerantMath = TolerantMath.Default; + private readonly IResampler sampler; private readonly int sourceLength; @@ -107,15 +109,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms scale = 1F; } - int radius = (int)Math.Ceiling(scale * sampler.Radius); + int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; - double center0 = (ratio - 1) * 0.5f; + double center0 = (ratio - 1) * 0.5; double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; - int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal); + int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); // corner case for cornerInteval: - // TODO: Implement library-wide utils for tolerant comparison - if (Math.Abs(firstNonNegativeLeftVal - cornerInterval) < 1e-8) + if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) { cornerInterval++; } @@ -167,13 +168,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms double center = ((destRowIndex + .5) * this.ratio) - .5; // Keep inside bounds. - int left = (int)Math.Ceiling(center - this.radius); + int left = (int)TolerantMath.Ceiling(center - this.radius); if (left < 0) { left = 0; } - int right = (int)Math.Floor(center + this.radius); + int right = (int)TolerantMath.Floor(center + this.radius); if (right > this.sourceLength - 1) { right = this.sourceLength - 1; diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 75ac7450c..86c1a7a25 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -1,7 +1,6 @@  - - netcoreapp2.1 + net462;net472;netcoreapp2.1 True latest full diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs index 31907b06d..54ac239ae 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs @@ -35,27 +35,29 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms scale = 1F; } - double radius = (double)Math.Ceiling(scale * sampler.Radius); + TolerantMath tolerantMath = TolerantMath.Default; + + double radius = tolerantMath.Ceiling(scale * sampler.Radius); var result = new List(); for (int i = 0; i < destinationSize; i++) { - if (i == 21 || i == 64) - { - Debug.Print("lol"); - } + //if (i == 21 || i == 64) + //{ + // Debug.Print("lol"); + //} double center = ((i + .5) * ratio) - .5; // Keep inside bounds. - int left = (int)Math.Ceiling(center - radius); + int left = (int)tolerantMath.Ceiling(center - radius); if (left < 0) { left = 0; } - int right = (int)Math.Floor(center + radius); + int right = (int)tolerantMath.Floor(center + radius); if (right > sourceSize - 1) { right = sourceSize - 1; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index dc7a441e9..c9f20cd27 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -119,7 +119,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } #endif - private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) { IResampler resampler = TestUtils.GetResampler(resamplerName); From 25195a74f14f4cf0d52422422a134920f55332c0 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 03:54:29 +0100 Subject: [PATCH 62/66] update submodule, skip debug-only test case --- .../Processing/Processors/Transforms/KernelMapTests.cs | 6 +++++- tests/Images/External | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs index c9f20cd27..783c3c5af 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -60,6 +60,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { nameof(KnownResamplers.Lanczos8), 100, 80 }, { nameof(KnownResamplers.Lanczos8), 10, 100 }, + // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: + { nameof(KnownResamplers.Box), 378, 149 }, + { nameof(KnownResamplers.Box), 349, 174 }, + // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData { nameof(KnownResamplers.Box), 201, 100 }, { nameof(KnownResamplers.Box), 199, 99 }, @@ -89,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms GenerateImageResizeData(); - [Theory] + [Theory(Skip = "Only for debugging and development")] [MemberData(nameof(KernelMapData))] public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) { diff --git a/tests/Images/External b/tests/Images/External index ed8a7b0b6..5b18d8c95 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit ed8a7b0b6fe1b2e2a7c822aa617103ae31192655 +Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128 From d4ff2a44f02ac703e32575a81f764805c257389b Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 04:14:46 +0100 Subject: [PATCH 63/66] remove outdated TODO note --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 45cb52fd9..9e03c0d3d 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -99,7 +99,6 @@ namespace SixLabors.ImageSharp /// /// Determine the Least Common Multiple (LCM) of two numbers. - /// TODO: This method might be useful for building a more compact /// public static int LeastCommonMultiple(int a, int b) { From 77e72c8000653fd806c6c4af412d27a8f436b050 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 14:51:06 +0100 Subject: [PATCH 64/66] test code cleanup --- ...esizeKernelMapTests.ReferenceKernelMap.cs} | 12 ++++------ ...nelMapTests.cs => ResizeKernelMapTests.cs} | 4 ++-- .../Processors/Transforms/ResizeTests.cs | 23 ------------------- 3 files changed, 6 insertions(+), 33 deletions(-) rename tests/ImageSharp.Tests/Processing/Processors/Transforms/{KernelMapTests.ReferenceKernelMap.cs => ResizeKernelMapTests.ReferenceKernelMap.cs} (93%) rename tests/ImageSharp.Tests/Processing/Processors/Transforms/{KernelMapTests.cs => ResizeKernelMapTests.cs} (98%) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs similarity index 93% rename from tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs rename to tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs index 54ac239ae..7d842c4e1 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs @@ -1,13 +1,14 @@ -using System; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - public partial class KernelMapTests + public partial class ResizeKernelMapTests { /// /// Simplified reference implementation for functionality. @@ -43,11 +44,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms for (int i = 0; i < destinationSize; i++) { - //if (i == 21 || i == 64) - //{ - // Debug.Print("lol"); - //} - double center = ((i + .5) * ratio) - .5; // Keep inside bounds. diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs similarity index 98% rename from tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs rename to tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 783c3c5af..08b294913 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -15,11 +15,11 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - public partial class KernelMapTests + public partial class ResizeKernelMapTests { private ITestOutputHelper Output { get; } - public KernelMapTests(ITestOutputHelper output) + public ResizeKernelMapTests(ITestOutputHelper output) { this.Output = output; } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 839d26e71..b4aff53e8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -58,29 +58,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } - // TODO: Merge with the previous theory + add test images - [Theory] - [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 1)] - [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 10)] - [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos3), 10)] - [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.Lanczos8), 10)] - [WithTestPatternImages(100, 100, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 10)] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 1)] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.NearestNeighbor), 5)] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, nameof(KnownResamplers.Bicubic), 5)] - public void ScaleUp(TestImageProvider provider, string samplerName, float ratio) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) - { - SizeF newSize = image.Size() * ratio; - image.Mutate(x => x.Resize((Size)newSize, TestUtils.GetResampler(samplerName), false)); - FormattableString details = $"{samplerName}_{ratio.ToString(System.Globalization.CultureInfo.InvariantCulture)}"; - - image.DebugSave(provider, details); - } - } - [Theory] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 1)] [WithFileCollection(nameof(CommonTestImages), DefaultPixelType, 4)] From 772cdda9780267e89d95e2a9d7ea504e1736745d Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 16:49:46 +0100 Subject: [PATCH 65/66] better comments + TolerantMath made readonly --- src/ImageSharp/Common/Helpers/TolerantMath.cs | 2 +- .../Transforms/Resize/ResizeKernelMap.cs | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs index d95aea9de..5347efcc0 100644 --- a/src/ImageSharp/Common/Helpers/TolerantMath.cs +++ b/src/ImageSharp/Common/Helpers/TolerantMath.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp /// Implements basic math operations using tolerant comparison /// whenever an equality check is needed. /// - internal struct TolerantMath + internal readonly struct TolerantMath { private readonly double epsilon; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 347aaf0be..f6edf9786 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -110,20 +110,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); + + // 'ratio' is a rational number. + // Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again". + // This value is determining the length of the periods in repeating kernel map rows. int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; + + // the center position at i == 0: double center0 = (ratio - 1) * 0.5; double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; + + // The number of rows building a "stairway" at the top and the bottom of the kernel map + // corresponding to the corners of the image. + // If we do not normalize the kernel values, these rows also fit the periodic logic, + // however, it's just simpler to calculate them separately. int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); - // corner case for cornerInteval: + // If firstNonNegativeLeftVal was an integral value, we don't need Ceiling: if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) { cornerInterval++; } - bool useMosaic = 2 * (cornerInterval + period) < destinationSize; + // If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization. + // If we don't have at least 2 periods, we go with the basic implementation: + bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; - ResizeKernelMap result = useMosaic + ResizeKernelMap result = hasAtLeast2Periods ? new PeriodicKernelMap( memoryAllocator, sampler, From ab4cae48ace9d4f8d0586b49241164a43ad76a4f Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 29 Nov 2018 17:03:46 +0100 Subject: [PATCH 66/66] improve comments again --- .../Processors/Transforms/Resize/ResizeKernelMap.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index f6edf9786..2ab574df2 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -104,9 +104,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms double ratio = (double)sourceSize / destinationSize; double scale = ratio; - if (scale < 1F) + if (scale < 1) { - scale = 1F; + scale = 1; } int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); @@ -126,7 +126,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // however, it's just simpler to calculate them separately. int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); - // If firstNonNegativeLeftVal was an integral value, we don't need Ceiling: + // If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1 + // instead of Ceiling: if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) { cornerInterval++;