From eb5c05efe1e107d5a560c6f8ed48126f53d50499 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 1 Aug 2024 21:46:58 +1000 Subject: [PATCH 1/4] Allow decoding Tiff of different frame size. --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Formats/Tiff/TiffDecoderCore.cs | 103 ++++++++++++------ .../Formats/Webp/WebpEncoderCore.cs | 4 +- src/ImageSharp/Image.cs | 2 +- src/ImageSharp/ImageFrame.cs | 20 ++-- .../ImageFrameCollection{TPixel}.cs | 2 +- src/ImageSharp/ImageFrame{TPixel}.cs | 2 +- src/ImageSharp/Image{TPixel}.cs | 4 +- .../Convolution/BokehBlurProcessor{TPixel}.cs | 4 +- .../Convolution2PassProcessor{TPixel}.cs | 2 +- .../ConvolutionProcessor{TPixel}.cs | 2 +- .../Effects/OilPaintingProcessor{TPixel}.cs | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 11 +- .../Tiff/TiffEncoderMultiframeTests.cs | 1 - .../ImageComparison/ExactImageComparer.cs | 3 +- .../ImageComparison/TolerantImageComparer.cs | 3 +- 17 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 11185d90b0..2e05ef782f 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -209,7 +209,7 @@ internal sealed class GifEncoderCore ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 4bbb68358f..978b9184e9 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -231,7 +231,7 @@ internal sealed class PngEncoderCore : IDisposable ImageFrame previousFrame = image.Frames.RootFrame; // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 2356d45e47..0ba9755b15 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore this.byteOrder = reader.ByteOrder; this.isBigTiff = reader.IsBigTiff; + Size? size = null; uint frameCount = 0; foreach (ExifProfile ifd in directories) { cancellationToken.ThrowIfCancellationRequested(); - ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); + ImageFrame frame = this.DecodeFrame(ifd, size, cancellationToken); + + if (!size.HasValue) + { + size = frame.Size; + } + frames.Add(frame); framesMetadata.Add(frame.Metadata); @@ -181,19 +188,8 @@ internal class TiffDecoderCore : ImageDecoderCore } } + this.Dimensions = frames[0].Size; ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); - - // TODO: Tiff frames can have different sizes. - ImageFrame root = frames[0]; - this.Dimensions = root.Size(); - foreach (ImageFrame frame in frames) - { - if (frame.Size() != root.Size()) - { - TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); - } - } - return new Image(this.configuration, metadata, frames); } catch @@ -235,9 +231,10 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The IFD tags. + /// The previously determined root frame size if decoded. /// The token to monitor cancellation. /// The tiff frame. - private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken) + private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); @@ -245,15 +242,29 @@ internal class TiffDecoderCore : ImageDecoderCore int width = GetImageWidth(tags); int height = GetImageHeight(tags); - ImageFrame frame = new(this.configuration, width, height, imageFrameMetaData); + + // If size has a value and the width/height off the tiff is smaller we much capture the delta. + if (size.HasValue) + { + if (size.Value.Width < width || size.Value.Height < height) + { + TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported."); + } + } + else + { + size = new Size(width, height); + } + + ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, imageFrameMetaData); if (isTiled) { - this.DecodeImageWithTiles(tags, frame, cancellationToken); + this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken); } else { - this.DecodeImageWithStrips(tags, frame, cancellationToken); + this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); } return frame; @@ -278,8 +289,10 @@ internal class TiffDecoderCore : ImageDecoderCore /// The pixel format. /// The IFD tags. /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The token to monitor cancellation. - private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int rowsPerStrip; @@ -302,6 +315,8 @@ internal class TiffDecoderCore : ImageDecoderCore { this.DecodeStripsPlanar( frame, + width, + height, rowsPerStrip, stripOffsets, stripByteCounts, @@ -311,6 +326,8 @@ internal class TiffDecoderCore : ImageDecoderCore { this.DecodeStripsChunky( frame, + width, + height, rowsPerStrip, stripOffsets, stripByteCounts, @@ -324,13 +341,13 @@ internal class TiffDecoderCore : ImageDecoderCore /// The pixel format. /// The IFD tags. /// The image frame to decode into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The token to monitor cancellation. - private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Buffer2D pixels = frame.PixelBuffer; - int width = pixels.Width; - int height = pixels.Height; if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue valueWidth)) { @@ -384,11 +401,20 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The number of rows per strip of data. /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The token to monitor cancellation. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsPlanar( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Channels; @@ -403,18 +429,18 @@ internal class TiffDecoderCore : ImageDecoderCore { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); + int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel); TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); for (int i = 0; i < stripsPerPlane; i++) { cancellationToken.ThrowIfCancellationRequested(); - int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip; int stripIndex = i; for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) @@ -430,7 +456,7 @@ internal class TiffDecoderCore : ImageDecoderCore stripIndex += stripsPerPlane; } - colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); + colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight); } } finally @@ -447,39 +473,48 @@ internal class TiffDecoderCore : ImageDecoderCore /// /// The pixel format. /// The image frame to decode data into. + /// The width in px units of the frame data. + /// The height in px units of the frame data. /// The rows per strip. /// The strip offsets. /// The strip byte counts. /// The token to monitor cancellation. - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsChunky( + ImageFrame frame, + int width, + int height, + int rowsPerStrip, + Span stripOffsets, + Span stripByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) { - rowsPerStrip = frame.Height; + rowsPerStrip = height; } - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); + int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); int bitsPerPixel = this.BitsPerPixel; using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { cancellationToken.ThrowIfCancellationRequested(); - int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 ? rowsPerStrip - : frame.Height % rowsPerStrip; + : height % rowsPerStrip; int top = rowsPerStrip * stripIndex; - if (top + stripHeight > frame.Height) + if (top + stripHeight > height) { // Make sure we ignore any strips that are not needed for the image (if too many are present). break; @@ -493,7 +528,7 @@ internal class TiffDecoderCore : ImageDecoderCore stripBufferSpan, cancellationToken); - colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); + colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index e37462fda4..733801d636 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -160,7 +160,7 @@ internal sealed class WebpEncoderCore // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { @@ -235,7 +235,7 @@ internal sealed class WebpEncoderCore // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size); for (int i = 1; i < image.Frames.Count; i++) { diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index d4f773abe1..9a5bd4cf06 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -77,7 +77,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// /// Gets the size of the image in px units. /// - public Size Size { get; internal set; } + public Size Size { get; private set; } /// /// Gets the bounds of the image. diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 2558e1a13a..3c5338fac3 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -25,20 +25,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) { this.Configuration = configuration; - this.Width = width; - this.Height = height; + this.Size = new(width, height); this.Metadata = metadata; } /// - /// Gets the width. + /// Gets the frame width in px units. /// - public int Width { get; private set; } + public int Width => this.Size.Width; /// - /// Gets the height. + /// Gets the frame height in px units. /// - public int Height { get; private set; } + public int Height => this.Size.Height; /// /// Gets the metadata of the frame. @@ -51,8 +50,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Gets the size of the frame. /// - /// The - public Size Size() => new(this.Width, this.Height); + public Size Size { get; private set; } /// /// Gets the bounds of the frame. @@ -80,9 +78,5 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// Updates the size of the image frame. /// /// The size. - internal void UpdateSize(Size size) - { - this.Width = size.Width; - this.Height = size.Height; - } + protected void UpdateSize(Size size) => this.Size = size; } diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index e927fb0fac..ad7d719744 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -414,7 +414,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer { ImageFrame result = new( this.parent.Configuration, - source.Size(), + source.Size, source.Metadata.DeepClone()); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); return result; diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 0b6354d05d..3877a2920f 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -322,7 +322,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// ImageFrame{TPixel}.CopyTo(): target must be of the same size! internal void CopyTo(Buffer2D target) { - if (this.Size() != target.Size()) + if (this.Size != target.Size()) { throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index e12631cbd7..1a50229ca9 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -419,9 +419,9 @@ public sealed class Image : Image ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames)); - Size rootSize = rootFrame.Size(); + Size rootSize = rootFrame.Size; - if (frames.Any(f => f.Size() != rootSize)) + if (frames.Any(f => f.Size != rootSize)) { throw new ArgumentException("The provided frames must be of the same size.", nameof(frames)); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index e4b0a60ab0..5931b7c402 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -96,7 +96,7 @@ internal class BokehBlurProcessor : ImageProcessor } // Create a 0-filled buffer to use to store the result of the component convolutions - using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size(), AllocationOptions.Clean); + using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size, AllocationOptions.Clean); // Perform the 1D convolutions on all the kernel components and accumulate the results this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer); @@ -134,7 +134,7 @@ internal class BokehBlurProcessor : ImageProcessor Buffer2D processingBuffer) { // Allocate the buffer with the intermediate convolution results - using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size); // Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width // to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation. diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index cc6e1e5fb2..10780a21e2 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -66,7 +66,7 @@ internal class Convolution2PassProcessor : ImageProcessor /// protected override void OnFrameApply(ImageFrame source) { - using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index d059ebe030..ae79f2c31d 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -51,7 +51,7 @@ internal class ConvolutionProcessor : ImageProcessor protected override void OnFrameApply(ImageFrame source) { MemoryAllocator allocator = this.Configuration.MemoryAllocator; - using Buffer2D targetPixels = allocator.Allocate2D(source.Size()); + using Buffer2D targetPixels = allocator.Allocate2D(source.Size); source.CopyTo(targetPixels); diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index 1491fe073b..f811bae0f7 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -38,7 +38,7 @@ internal class OilPaintingProcessor : ImageProcessor int levels = Math.Clamp(this.definition.Levels, 1, 255); int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height)); - using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size()); + using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size); source.CopyTo(targetPixels); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ab49805a35..614a8a1f17 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -23,7 +23,6 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; [Theory] - [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) @@ -596,6 +595,16 @@ public class TiffDecoderTests : TiffDecoderBaseTester Assert.Equal(1, image.Frames.Count); } + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void CanDecode_MultiFrameMipMap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + image.DebugSaveMultiFrame(provider); + } + [Theory] [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs index dce6ebc38f..716b978a71 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -18,7 +18,6 @@ public class TiffEncoderMultiframeTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); [Theory] - [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs index aa8ab397d2..92fc06eff5 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -16,7 +15,7 @@ public class ExactImageComparer : ImageComparer ImageFrame expected, ImageFrame actual) { - if (expected.Size() != actual.Size()) + if (expected.Size != actual.Size) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index 93ed4c6fff..d057267da7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -58,7 +57,7 @@ public class TolerantImageComparer : ImageComparer public override ImageSimilarityReport CompareImagesOrFrames(int index, ImageFrame expected, ImageFrame actual) { - if (expected.Size() != actual.Size()) + if (expected.Size != actual.Size) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } From e1555fd4ba6a972b7b0121b22bf502ebb49a9606 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 5 Aug 2024 23:46:53 +1000 Subject: [PATCH 2/4] Fix transform bounds calculations --- .../Processing/AffineTransformBuilder.cs | 38 +-- .../Linear/LinearTransformUtility.cs | 4 +- .../Transforms/Linear/RotateProcessor.cs | 5 +- .../Transforms/Linear/SkewProcessor.cs | 5 +- .../Processors/Transforms/TransformUtils.cs | 274 +++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 39 +-- .../Transforms/AffineTransformTests.cs | 18 ++ .../Transforms/ProjectiveTransformTests.cs | 10 +- 8 files changed, 191 insertions(+), 202 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 59264698bd..e8c628ff12 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a rotation matrix using the given rotation angle in degrees @@ -31,8 +30,7 @@ public class AffineTransformBuilder /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -68,9 +66,7 @@ public class AffineTransformBuilder /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -145,9 +141,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -156,9 +150,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -187,9 +179,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -198,9 +188,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -267,7 +255,7 @@ public class AffineTransformBuilder public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -283,7 +271,7 @@ public class AffineTransformBuilder public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -340,13 +328,13 @@ public class AffineTransformBuilder // Translate the origin matrix to cater for source rectangle offsets. Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix3x2 matrix) @@ -357,17 +345,15 @@ public class AffineTransformBuilder } } - private AffineTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private AffineTransformBuilder Append(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs index b5eb202c18..1f68e32744 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs @@ -43,7 +43,7 @@ internal static class LinearTransformUtility /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeStart(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max); + => Numerics.Clamp((int)MathF.Floor(center - radius), min, max); /// /// Gets the end position (inclusive) for a sampling range given @@ -56,5 +56,5 @@ internal static class LinearTransformUtility /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeEnd(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Floor(center + radius), min, max); + => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index 6580636a24..aee7fd7816 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -29,14 +29,13 @@ public sealed class RotateProcessor : AffineTransformProcessor public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), - TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor - private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 97b18de6c8..085d2bbec1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -31,7 +31,6 @@ public sealed class SkewProcessor : AffineTransformProcessor public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), - TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize), sampler, sourceSize) { @@ -40,8 +39,8 @@ public sealed class SkewProcessor : AffineTransformProcessor } // Helper constructor: - private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 70112ab5a8..787e7fc3e3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -68,6 +68,11 @@ internal static class TransformUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) { + // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases, + // such as when the point is transformed behind the camera in a perspective projection. + // However, in many 2D contexts, negative w values are not meaningful and could cause issues + // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure + // we don't divide by a very small or negative number, effectively treating any negative w as epsilon. const float epsilon = 0.0000001F; Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); @@ -81,9 +86,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. @@ -93,33 +96,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size. - /// - /// The amount of rotation, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in radians and the source size. - /// - /// The amount of rotation, in radians. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -130,9 +107,7 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -143,78 +118,28 @@ internal static class TransformUtils /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); - - /// - /// Creates a centered skew bounds matrix from the give angles in degrees and the source size. - /// - /// The X angle, in degrees. - /// The Y angle, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); - - /// - /// Creates a centered skew bounds 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 . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); /// /// Gets the centered transform matrix based upon the source rectangle. /// - /// The source image bounds. - /// The transformation matrix. - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - 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); - - // Centered transforms must be 0 based so we offset the bounds width and height. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F); - - // Translate back to world space. - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - return centered; - } - - /// - /// Gets the centered bounds matrix based upon the source rectangle. - /// - /// The source image bounds. /// The transformation matrix. + /// The source image size. /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredBoundsMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + Size destinationSize = GetUnboundedTransformedSize(matrix, size); // 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); - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + // The source size is provided using the coordinate space of the source image. + // however the transform should always be applied in the pixel space. + // To account for this we offset by the size - 1 to translate to the pixel space. + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -236,6 +161,12 @@ internal static class TransformUtils { Matrix4x4 matrix = Matrix4x4.Identity; + // The source size is provided using the Coordinate/Geometric space of the source image. + // However, the transform should always be applied in the Discrete/Pixel space to ensure + // that the transformation fully encompasses all pixels without clipping at the edges. + // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. + // size -= new Size(1, 1); + /* * SkMatrix is laid out in the following manner: * @@ -345,52 +276,101 @@ internal static class TransformUtils } /// - /// Returns the rectangle bounds relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) - { - Rectangle transformed = GetTransformedRectangle(rectangle, matrix); - return new Rectangle(0, 0, transformed.Width, transformed.Height); - } + public static Size GetTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, true); /// - /// Returns the rectangle relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size GetTransformedSize(Matrix4x4 matrix, Size size) { - if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(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 rectangle; + return size; } - Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); - Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); - Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); - Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + // Check if the matrix involves only affine transformations by inspecting the relevant components. + // We want to use pixel space for calculations only if the transformation is purely 2D and does not include + // any perspective effects, non-standard scaling, or unusual translations that could distort the image. + // The conditions are as follows: + bool usePixelSpace = + + // 1. Ensure there's no perspective distortion: + // M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0. + (matrix.M34 == 0) && + + // 2. Ensure standard affine transformation without any unusual depth or perspective scaling: + // M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth + // scaling or perspective, which suggests a more complex transformation. + (matrix.M44 == 1) && + + // 3. Ensure no unusual translation in the x-direction: + // M14 represents translation in the x-direction that might be part of a more complex transformation. + // For standard affine transformations, M14 should be 0. + (matrix.M14 == 0) && - return GetBoundingRectangle(tl, tr, bl, br); + // 4. Ensure no unusual translation in the y-direction: + // M24 represents translation in the y-direction that might be part of a more complex transformation. + // For standard affine transformations, M24 should be 0. + (matrix.M24 == 0); + + // Define an offset size to translate between pixel space and coordinate space. + // When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates. + // When not using pixel space, use SizeF.Empty as the offset. + + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + + // Apply the offset relative to the scale + SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty; + + // Subtract the offset size to translate to the appropriate space (pixel or coordinate). + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the correct space. + return Size.Ceiling(ConstrainSize(bounds) + offsetSize); + } + + return size; } /// /// Returns the size relative to the source for the given transformation matrix. /// + /// The transformation matrix. /// The source size. + /// + /// The . + /// + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, false); + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// /// The transformation matrix. + /// The source size. + /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - public static Size GetTransformedSize(Size size, Matrix3x2 matrix) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -399,9 +379,20 @@ internal static class TransformUtils return size; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + // Define an offset size to translate between coordinate space and pixel space. + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + SizeF offsetSize = new(scaleX, scaleY); + + // Subtract the offset size to translate to the pixel space. + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the coordinate space. + return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize); + } - return ConstrainSize(rectangle); + return size; } /// @@ -409,46 +400,52 @@ internal static class TransformUtils /// /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) { - if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) { - return rectangle; + bounds = default; + return false; } - Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); - Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); - Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); - Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); + Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); - return GetBoundingRectangle(tl, tr, bl, br); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } /// - /// Returns the size relative to the source for the given transformation matrix. + /// Returns the rectangle relative to the source for the given transformation matrix. /// - /// The source size. + /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Size GetTransformedSize(Size size, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) { - Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); - - if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) { - return size; + bounds = default; + return false; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); + Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); + Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); + Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); - return ConstrainSize(rectangle); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -482,6 +479,11 @@ internal static class TransformUtils 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)); + // Clamp the values to the nearest whole pixel. + return Rectangle.FromLTRB( + (int)Math.Floor(left), + (int)Math.Floor(top), + (int)Math.Ceiling(right), + (int)Math.Ceiling(bottom)); } } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 0387adebb9..06e6f0e71a 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a matrix that performs a tapering projective transform. @@ -22,9 +21,7 @@ public class ProjectiveTransformBuilder /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Prepend( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -34,9 +31,7 @@ public class ProjectiveTransformBuilder /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Append( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -52,9 +47,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -88,9 +81,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -174,9 +165,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -214,9 +203,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -283,7 +270,7 @@ public class ProjectiveTransformBuilder public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -299,7 +286,7 @@ public class ProjectiveTransformBuilder public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -357,13 +344,13 @@ public class ProjectiveTransformBuilder // Translate the origin matrix to cater for source rectangle offsets. Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix4x4 matrix) @@ -374,17 +361,15 @@ public class ProjectiveTransformBuilder } } - private ProjectiveTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private ProjectiveTransformBuilder Append(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index 895cf60fd3..05604ac6e6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -231,6 +231,24 @@ public class AffineTransformTests Assert.Equal(100, image.Height); } + [Theory] + [WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)] + public void Issue2753(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + AffineTransformBuilder builder = + new AffineTransformBuilder().AppendRotationDegrees(270, new Vector2(3.5f, 3.5f)); + image.Mutate(x => x.BackgroundColor(Color.Red)); + image.Mutate(x => x = x.Transform(builder)); + + image.DebugSave(provider); + + Assert.Equal(4, image.Width); + Assert.Equal(8, image.Height); + } + [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] public void Identity(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 21eda034ea..128df01a06 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -128,11 +128,11 @@ public class ProjectiveTransformTests using (Image image = provider.GetImage()) { #pragma warning disable SA1117 // Parameters should be on same line or separate lines - var matrix = new Matrix4x4( - 0.260987f, -0.434909f, 0, -0.0022184f, - 0.373196f, 0.949882f, 0, -0.000312129f, - 0, 0, 1, 0, - 52, 165, 0, 1); + Matrix4x4 matrix = new( + 0.260987f, -0.434909f, 0, -0.0022184f, + 0.373196f, 0.949882f, 0, -0.000312129f, + 0, 0, 1, 0, + 52, 165, 0, 1); #pragma warning restore SA1117 // Parameters should be on same line or separate lines ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() From 4d67be8b860ade92b20487aa6ece94283e22e661 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 9 Aug 2024 00:04:29 +1000 Subject: [PATCH 3/4] Can encode Tiff MipMaps --- .../Formats/AnimatedImageFrameMetadata.cs | 32 ---- .../Formats/AnimatedImageMetadata.cs | 33 ---- src/ImageSharp/Formats/Bmp/BmpMetadata.cs | 6 + src/ImageSharp/Formats/Cur/CurDecoderCore.cs | 4 - .../Formats/Cur/CurFrameMetadata.cs | 20 +++ src/ImageSharp/Formats/Cur/CurMetadata.cs | 32 +--- .../Formats/Gif/GifFrameMetadata.cs | 37 +---- src/ImageSharp/Formats/Gif/GifMetadata.cs | 6 + .../Formats/IFormatFrameMetadata.cs | 11 ++ src/ImageSharp/Formats/IFormatMetadata.cs | 8 + src/ImageSharp/Formats/Ico/IcoDecoderCore.cs | 2 - .../Formats/Ico/IcoFrameMetadata.cs | 20 +++ src/ImageSharp/Formats/Ico/IcoMetadata.cs | 20 +-- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 6 + src/ImageSharp/Formats/Pbm/PbmMetadata.cs | 6 + .../Formats/Png/PngFrameMetadata.cs | 7 + src/ImageSharp/Formats/Png/PngMetadata.cs | 6 + src/ImageSharp/Formats/Qoi/QoiMetadata.cs | 6 + src/ImageSharp/Formats/Tga/TgaMetadata.cs | 6 + .../Formats/Tiff/TiffDecoderCore.cs | 67 +++----- .../Formats/Tiff/TiffEncoderCore.cs | 18 +- .../Tiff/TiffEncoderEntriesCollector.cs | 10 +- .../Formats/Tiff/TiffFrameMetadata.cs | 155 +++++++++++++----- src/ImageSharp/Formats/Tiff/TiffMetadata.cs | 6 + .../Writers/TiffBaseColorWriter{TPixel}.cs | 27 ++- .../Tiff/Writers/TiffBiColorWriter{TPixel}.cs | 19 ++- .../Tiff/Writers/TiffColorWriterFactory.cs | 28 ++-- .../TiffCompositeColorWriter{TPixel}.cs | 21 +-- .../Tiff/Writers/TiffGrayL16Writer{TPixel}.cs | 12 +- .../Tiff/Writers/TiffGrayWriter{TPixel}.cs | 12 +- .../Tiff/Writers/TiffPaletteWriter{TPixel}.cs | 7 +- .../Tiff/Writers/TiffRgbWriter{TPixel}.cs | 12 +- .../Formats/Webp/WebpFrameMetadata.cs | 8 + src/ImageSharp/Formats/Webp/WebpMetadata.cs | 6 + src/ImageSharp/Image.cs | 8 +- src/ImageSharp/ImageFrame.cs | 12 +- src/ImageSharp/ImageFrame{TPixel}.cs | 22 ++- src/ImageSharp/Image{TPixel}.cs | 32 +++- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 33 +++- src/ImageSharp/Metadata/ImageMetadata.cs | 16 ++ .../Metadata/Profiles/Exif/ExifProfile.cs | 14 ++ .../CloningImageProcessor{TPixel}.cs | 6 +- .../Transforms/TransformProcessorHelpers.cs | 39 ----- ...essor.cs => TransformProcessor{TPixel}.cs} | 9 +- .../Formats/Tiff/TiffEncoderTests.cs | 77 +++++++++ .../Transforms/TransformsHelpersTest.cs | 35 ---- 46 files changed, 598 insertions(+), 381 deletions(-) delete mode 100644 src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs delete mode 100644 src/ImageSharp/Formats/AnimatedImageMetadata.cs delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs rename src/ImageSharp/Processing/Processors/Transforms/{TransformProcessor.cs => TransformProcessor{TPixel}.cs} (80%) delete mode 100644 tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs deleted file mode 100644 index 8f8e187403..0000000000 --- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats; - -internal class AnimatedImageFrameMetadata -{ - /// - /// Gets or sets the frame color table. - /// - public ReadOnlyMemory? ColorTable { get; set; } - - /// - /// Gets or sets the frame color table mode. - /// - public FrameColorTableMode ColorTableMode { get; set; } - - /// - /// Gets or sets the duration of the frame. - /// - public TimeSpan Duration { get; set; } - - /// - /// Gets or sets the frame alpha blending mode. - /// - public FrameBlendMode BlendMode { get; set; } - - /// - /// Gets or sets the frame disposal mode. - /// - public FrameDisposalMode DisposalMode { get; set; } -} diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs deleted file mode 100644 index ac3ca29f4f..0000000000 --- a/src/ImageSharp/Formats/AnimatedImageMetadata.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats; - -internal class AnimatedImageMetadata -{ - /// - /// Gets or sets the shared color table. - /// - public ReadOnlyMemory? ColorTable { get; set; } - - /// - /// Gets or sets the shared color table mode. - /// - public FrameColorTableMode ColorTableMode { get; set; } - - /// - /// Gets or sets the default background color of the canvas when animating. - /// This color may be used to fill the unused space on the canvas around the frames, - /// as well as the transparent pixels of the first frame. - /// The background color is also used when the disposal mode is . - /// - public Color BackgroundColor { get; set; } - - /// - /// Gets or sets the number of times any animation is repeated. - /// - /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1. - /// - /// - public ushort RepeatCount { get; set; } -} diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 68e99bdc5f..d0c60421c4 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata /// public BmpMetadata DeepClone() => new(this); + + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } } diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index a8a51878e0..6fc8905279 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore curMetadata.Compression = compression; curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.ColorTable = colorTable; - curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; - curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; - curMetadata.HotspotX = curFrameMetadata.HotspotX; - curMetadata.HotspotY = curFrameMetadata.HotspotY; } } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 06cf426dc4..4e9a432b16 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata EncodingHeight = this.EncodingHeight }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata ColorType = color }; } + + private static byte Scale(byte? value, int destination, float ratio) + { + if (value is null) + { + return (byte)Math.Clamp(destination, 0, 255); + } + + return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + } } diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index 6e97a8584a..19de7f434d 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs @@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata private CurMetadata(CurMetadata other) { this.Compression = other.Compression; - this.HotspotX = other.HotspotX; - this.HotspotY = other.HotspotY; - this.EncodingWidth = other.EncodingWidth; - this.EncodingHeight = other.EncodingHeight; this.BmpBitsPerPixel = other.BmpBitsPerPixel; if (other.ColorTable?.Length > 0) @@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata /// public IconFrameCompression Compression { get; set; } - /// - /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame. - /// - public ushort HotspotX { get; set; } - - /// - /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame. - /// - public ushort HotspotY { get; set; } - - /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- public byte EncodingWidth { get; set; } - - /// - /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- public byte EncodingHeight { get; set; } - /// /// Gets or sets the number of bits per pixel.
/// Used when is @@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata ColorTable = this.ColorTable }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index f81329e973..5fe892c656 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata }; } + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// public GifFrameMetadata DeepClone() => new(this); - - internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) - { - // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table? - int index = -1; - const float background = 1f; - if (metadata.ColorTable.HasValue) - { - ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; - for (int i = 0; i < colorTable.Length; i++) - { - Vector4 vector = colorTable[i].ToScaledVector4(); - if (vector.W < background) - { - index = i; - } - } - } - - bool hasTransparency = index >= 0; - - return new() - { - LocalColorTable = metadata.ColorTable, - ColorTableMode = metadata.ColorTableMode, - FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMode = metadata.DisposalMode, - HasTransparency = hasTransparency, - TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, - }; - } } diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 565038b55a..517609af45 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata }; } + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs index 4eef93ad34..20f27d050c 100644 --- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs +++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats; /// @@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable /// /// The . FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel; } /// diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs index 8d695306e4..a351431c94 100644 --- a/src/ImageSharp/Formats/IFormatMetadata.cs +++ b/src/ImageSharp/Formats/IFormatMetadata.cs @@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable /// /// The . FormatConnectingMetadata ToFormatConnectingMetadata(); + + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image . + void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel; } /// diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index 8b59974eb3..b8a1dded15 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore curMetadata.Compression = compression; curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.ColorTable = colorTable; - curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; - curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; } } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index c244e38981..a2d1c01391 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata EncodingHeight = this.EncodingHeight }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata ColorType = color }; } + + private static byte Scale(byte? value, int destination, float ratio) + { + if (value is null) + { + return (byte)Math.Clamp(destination, 0, 255); + } + + return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + } } diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index 7e31468ecc..a6c2704b31 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata private IcoMetadata(IcoMetadata other) { this.Compression = other.Compression; - this.EncodingWidth = other.EncodingWidth; - this.EncodingHeight = other.EncodingHeight; this.BmpBitsPerPixel = other.BmpBitsPerPixel; if (other.ColorTable?.Length > 0) @@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata /// public IconFrameCompression Compression { get; set; } - /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- public byte EncodingWidth { get; set; } - - /// - /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. - ///
- public byte EncodingHeight { get; set; } - /// /// Gets or sets the number of bits per pixel.
/// Used when is @@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata ColorTable = this.ColorTable }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index f2f34ec496..fe4855dc77 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata Quality = this.Quality, }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs index fec4beda7c..d852f3c8eb 100644 --- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs +++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs @@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo(), }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index c142a1c8e0..b8086cd6d1 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata }; } + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index a7b3672ef5..00cba088cb 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs index e2062014d7..e463d511d2 100644 --- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs +++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs @@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs index 58b5119523..8d40f86464 100644 --- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs +++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs @@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 0ba9755b15..d699a7b631 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -211,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore IList directories = reader.Read(); List framesMetadata = []; - foreach (ExifProfile dir in directories) + int width = 0; + int height = 0; + + for (int i = 0; i < directories.Count; i++) { - framesMetadata.Add(this.CreateFrameMetadata(dir)); - } + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta + = this.CreateFrameMetadata(directories[i]); - ExifProfile rootFrameExifProfile = directories[0]; + framesMetadata.Add(meta.FrameMetadata); - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); + width = Math.Max(width, meta.TiffMetadata.EncodingWidth); + height = Math.Max(height, meta.TiffMetadata.EncodingHeight); + } - int width = GetImageWidth(rootFrameExifProfile); - int height = GetImageHeight(rootFrameExifProfile); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); return new ImageInfo(new(width, height), metadata, framesMetadata); } @@ -237,11 +241,11 @@ internal class TiffDecoderCore : ImageDecoderCore private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); - bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata()); + (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags); + bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata); - int width = GetImageWidth(tags); - int height = GetImageHeight(tags); + int width = metadata.TiffFrameMetadata.EncodingWidth; + int height = metadata.TiffFrameMetadata.EncodingHeight; // If size has a value and the width/height off the tiff is smaller we much capture the delta. if (size.HasValue) @@ -256,7 +260,7 @@ internal class TiffDecoderCore : ImageDecoderCore size = new Size(width, height); } - ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, imageFrameMetaData); + ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata); if (isTiled) { @@ -270,7 +274,7 @@ internal class TiffDecoderCore : ImageDecoderCore return frame; } - private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags) + private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags) { ImageFrameMetadata imageFrameMetaData = new(); if (!this.skipMetadata) @@ -278,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore imageFrameMetaData.ExifProfile = tags; } - TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags); + TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags); + imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata); - return imageFrameMetaData; + return (imageFrameMetaData, tiffMetadata); } /// @@ -825,38 +830,6 @@ internal class TiffDecoderCore : ImageDecoderCore return bytesPerRow * height; } - /// - /// Gets the width of the image frame. - /// - /// The image frame exif profile. - /// The image width. - private static int GetImageWidth(ExifProfile exifProfile) - { - if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue width)) - { - TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth"); - } - - DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); - - return (int)width.Value; - } - - /// - /// Gets the height of the image frame. - /// - /// The image frame exif profile. - /// The image height. - private static int GetImageHeight(ExifProfile exifProfile) - { - if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue height)) - { - TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); - } - - return (int)height.Value; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 5f91fd7393..b560067f3f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore long ifdOffset) where TPixel : unmanaged, IPixel { + // Get the width and height of the frame. + // This can differ from the frame bounds in-memory if the image represents only + // a subregion. + TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata(); + int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width; + int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height; + + width = Math.Min(width, frame.Width); + height = Math.Min(height, frame.Height); + Size encodingSize = new(width, height); + using TiffBaseCompressor compressor = TiffCompressorFactory.Create( compression, writer.BaseStream, this.memoryAllocator, - frame.Width, + width, (int)bitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); @@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, frame, + encodingSize, this.quantizer, this.pixelSamplingStrategy, this.memoryAllocator, @@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore entriesCollector, (int)bitsPerPixel); - int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); + int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType); colorWriter.Write(compressor, rowsPerStrip); @@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore // Write the metadata for the frame entriesCollector.ProcessMetadata(frame, this.skipMetadata); - entriesCollector.ProcessFrameInfo(frame, imageMetadata); + entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata); entriesCollector.ProcessImageFormat(this); if (writer.Position % 2 != 0) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index c8e28111ec..803b77fb0a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector public void ProcessMetadata(ImageFrame frame, bool skipMetadata) => new MetadataProcessor(this).Process(frame, skipMetadata); - public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) - => new FrameInfoProcessor(this).Process(frame, imageMetadata); + public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector { } - public void Process(ImageFrame frame, ImageMetadata imageMetadata) + public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata) { this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) { - Value = (uint)frame.Width + Value = (uint)encodingSize.Width }); this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) { - Value = (uint)frame.Height + Value = (uint)encodingSize.Height }); this.ProcessResolution(imageMetadata); diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs index bb5da37411..189fee8b0c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata this.PhotometricInterpretation = other.PhotometricInterpretation; this.Predictor = other.Predictor; this.InkSet = other.InkSet; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; } /// @@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata /// public TiffInkSet? InkSet { get; set; } + /// + /// Gets or sets the encoding width. + /// + public int EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height. + /// + public int EncodingHeight { get; set; } + /// public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) - => new(); + { + TiffFrameMetadata frameMetadata = new(); + if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue) + { + frameMetadata.EncodingWidth = metadata.EncodingWidth.Value; + frameMetadata.EncodingHeight = metadata.EncodingHeight.Value; + } + + return frameMetadata; + } /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - => new(); + => new() + { + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY); + + // Overwrite the EXIF dimensional metadata with the encoding dimensions of the image. + destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight); + } + + private static int Scale(int value, int destination, float ratio) + { + if (value <= 0) + { + return destination; + } + + return Math.Min((int)MathF.Ceiling(value * ratio), destination); + } /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); @@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata /// /// The tiff frame meta data. /// The Exif profile containing tiff frame directory tags. - internal static void Parse(TiffFrameMetadata meta, ExifProfile profile) + private static void Parse(TiffFrameMetadata meta, ExifProfile profile) { - if (profile != null) + meta.EncodingWidth = GetImageWidth(profile); + meta.EncodingHeight = GetImageHeight(profile); + + if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue) + && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)) + { + meta.BitsPerSample = bitsPerSample; + } + + meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); + + if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) { - if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue) - && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample)) - { - meta.BitsPerSample = bitsPerSample; - } - - meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel(); - - if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue)) - { - meta.Compression = (TiffCompression)compressionValue.Value; - } - - if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue)) - { - meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value; - } - - if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue)) - { - meta.Predictor = (TiffPredictor)predictorValue.Value; - } - - if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue)) - { - meta.InkSet = (TiffInkSet)inkSetValue.Value; - } - - // TODO: Why do we remove this? Encoding should overwrite. - profile.RemoveValue(ExifTag.BitsPerSample); - profile.RemoveValue(ExifTag.Compression); - profile.RemoveValue(ExifTag.PhotometricInterpretation); - profile.RemoveValue(ExifTag.Predictor); + meta.Compression = (TiffCompression)compressionValue.Value; } + + if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue)) + { + meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value; + } + + if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue)) + { + meta.Predictor = (TiffPredictor)predictorValue.Value; + } + + if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue)) + { + meta.InkSet = (TiffInkSet)inkSetValue.Value; + } + + // Remove values, we've explicitly captured them and they could change on encode. + profile.RemoveValue(ExifTag.BitsPerSample); + profile.RemoveValue(ExifTag.Compression); + profile.RemoveValue(ExifTag.PhotometricInterpretation); + profile.RemoveValue(ExifTag.Predictor); + } + + /// + /// Gets the width of the image frame. + /// + /// The image frame exif profile. + /// The image width. + private static int GetImageWidth(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue? width)) + { + TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth"); + } + + DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); + + return (int)width.Value; + } + + /// + /// Gets the height of the image frame. + /// + /// The image frame exif profile. + /// The image height. + private static int GetImageHeight(ExifProfile exifProfile) + { + if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue? height)) + { + TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); + } + + return (int)height.Value; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index cc70941d51..e965fcb4f6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata PixelTypeInfo = this.GetPixelTypeInfo() }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs index c4a7492553..9fd730f416 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs @@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter : IDisposable { private bool isDisposed; - protected TiffBaseColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + protected TiffBaseColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) { + this.Width = encodingSize.Width; + this.Height = encodingSize.Height; this.Image = image; this.MemoryAllocator = memoryAllocator; this.Configuration = configuration; @@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter : IDisposable ///
public abstract int BitsPerPixel { get; } + /// + /// Gets the width of the portion of the image to be encoded. + /// + public int Width { get; } + + /// + /// Gets the height of the portion of the image to be encoded. + /// + public int Height { get; } + /// /// Gets the bytes per row. /// - public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8); + public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8); protected ImageFrame Image { get; } @@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter : IDisposable public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) { DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer"); - int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip; + int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip; uint[] stripOffsets = new uint[stripsCount]; uint[] stripByteCounts = new uint[stripsCount]; int stripIndex = 0; compressor.Initialize(rowsPerStrip); - for (int y = 0; y < this.Image.Height; y += rowsPerStrip) + for (int y = 0; y < this.Height; y += rowsPerStrip) { long offset = compressor.Output.Position; - int height = Math.Min(rowsPerStrip, this.Image.Height - y); + int height = Math.Min(rowsPerStrip, this.Height - y); this.EncodeStrip(y, height, compressor); long endOffset = compressor.Output.Position; diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index a6f4c31060..647ff8a1a3 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter private IMemoryOwner bitStrip; - public TiffBiColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffBiColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { // Convert image to black and white. - this.imageBlackWhite = new Image(configuration, new ImageMetadata(), new[] { image.Clone() }); + this.imageBlackWhite = new Image(configuration, new ImageMetadata(), [image.Clone()]); this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); } @@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter /// protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - int width = this.Image.Width; + int width = this.Width; - if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax) + if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax) { // Special case for T4BitCompressor. int stripPixels = width * height; @@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter int bitIndex = 0; int byteIndex = 0; Span outputRow = rows[(outputRowIdx * this.BytesPerRow)..]; - Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row); + Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width]; PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); - for (int x = 0; x < this.Image.Width; x++) + for (int x = 0; x < this.Width; x++) { int shift = 7 - bitIndex; if (pixelAsGraySpan[x] == 255) diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs index 96c8aeb324..31a1b0e414 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory public static TiffBaseColorWriter Create( TiffPhotometricInterpretation? photometricInterpretation, ImageFrame image, + Size encodingSize, IQuantizer quantizer, IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, @@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) where TPixel : unmanaged, IPixel - { - switch (photometricInterpretation) + => photometricInterpretation switch { - case TiffPhotometricInterpretation.PaletteColor: - return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); - case TiffPhotometricInterpretation.BlackIsZero: - case TiffPhotometricInterpretation.WhiteIsZero: - return bitsPerPixel switch - { - 1 => new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector), - 16 => new TiffGrayL16Writer(image, memoryAllocator, configuration, entriesCollector), - _ => new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector) - }; - - default: - return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector); - } - } + TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel), + TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch + { + 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector), + _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector) + }, + _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector), + }; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs index 007857148a..67dde493c5 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs @@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; /// /// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr). /// +/// The tpe of pixel format. internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter where TPixel : unmanaged, IPixel { private IMemoryOwner rowBuffer; - protected TiffCompositeColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + protected TiffCompositeColorWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - if (this.rowBuffer == null) - { - this.rowBuffer = this.MemoryAllocator.Allocate(this.BytesPerRow * height); - } - - this.rowBuffer.Clear(); + (this.rowBuffer ??= this.MemoryAllocator.Allocate(this.BytesPerRow * height)).Clear(); Span outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)]; - int width = this.Image.Width; + int width = this.Width; using IMemoryOwner stripPixelBuffer = this.MemoryAllocator.Allocate(height * width); Span stripPixels = stripPixelBuffer.GetSpan(); int lastRow = y + height; int stripPixelsRowIdx = 0; for (int row = y; row < lastRow; row++) { - Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row); + Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width]; stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); stripPixelsRowIdx++; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs index 3e0e074e95..857f551f41 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffGrayL16Writer(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffGrayL16Writer( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter 16; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs index b2a476b9aa..4a037f0d33 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffGrayWriter : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffGrayWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffGrayWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter : TiffCompositeColorWriter public override int BitsPerPixel => 8; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs index d9a0960d9b..da66373631 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter public TiffPaletteWriter( ImageFrame frame, + Size encodingSize, IQuantizer quantizer, IPixelSamplingStrategy pixelSamplingStrategy, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector, int bitsPerPixel) - : base(frame, memoryAllocator, configuration, entriesCollector) + : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector) { DebugGuard.NotNull(quantizer, nameof(quantizer)); DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); @@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter }); frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); - this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize)); this.AddColorMapTag(); } @@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter /// protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) { - int width = this.Image.Width; + int width = this.quantizedFrame.Width; if (this.BitsPerPixel == 4) { diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs index 3494b6ceae..93c46a92e4 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers; internal sealed class TiffRgbWriter : TiffCompositeColorWriter where TPixel : unmanaged, IPixel { - public TiffRgbWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) - : base(image, memoryAllocator, configuration, entriesCollector) + public TiffRgbWriter( + ImageFrame image, + Size encodingSize, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector) + : base(image, encodingSize, memoryAllocator, configuration, entriesCollector) { } @@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter : TiffCompositeColorWriter public override int BitsPerPixel => 24; /// - protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); + protected override void EncodePixels(Span pixels, Span buffer) + => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index 45e182d223..3865f9837f 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Webp; /// @@ -61,6 +63,12 @@ public class WebpFrameMetadata : IFormatFrameMetadata BlendMode = this.BlendMethod, }; + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 33ebbbf6dc..db57bd8f27 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -145,6 +145,12 @@ public class WebpMetadata : IFormatMetadata BackgroundColor = this.BackgroundColor }; + /// + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + } + /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 9a5bd4cf06..07b40a41a1 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -72,7 +72,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// /// Gets any metadata associated with the image. /// - public ImageMetadata Metadata { get; } + public ImageMetadata Metadata { get; private set; } /// /// Gets the size of the image in px units. @@ -185,6 +185,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The . protected void UpdateSize(Size size) => this.Size = size; + /// + /// Updates the metadata of the image after mutation. + /// + /// The . + protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata; + /// /// Disposes the object and frees resources for the Garbage Collector. /// diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 3c5338fac3..fdde5019e1 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -42,7 +42,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Gets the metadata of the frame. /// - public ImageFrameMetadata Metadata { get; } + public ImageFrameMetadata Metadata { get; private set; } /// public Configuration Configuration { get; } @@ -75,8 +75,14 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable where TDestinationPixel : unmanaged, IPixel; /// - /// Updates the size of the image frame. + /// Updates the size of the image frame after mutation. /// - /// The size. + /// The . protected void UpdateSize(Size size) => this.Size = size; + + /// + /// Updates the metadata of the image frame after mutation. + /// + /// The . + protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata; } diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 3877a2920f..2287f65cd8 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -331,17 +331,29 @@ public sealed class ImageFrame : ImageFrame, IPixelSource } /// - /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. + /// Switches the buffers used by the image and the pixel source meaning that the Image will "own" the buffer + /// from the pixelSource and the pixel source will now own the Image buffer. /// - /// The pixel source. - internal void SwapOrCopyPixelsBufferFrom(ImageFrame pixelSource) + /// The pixel source. + internal void SwapOrCopyPixelsBufferFrom(ImageFrame source) { - Guard.NotNull(pixelSource, nameof(pixelSource)); + Guard.NotNull(source, nameof(source)); - Buffer2D.SwapOrCopyContent(this.PixelBuffer, pixelSource.PixelBuffer); + Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); this.UpdateSize(this.PixelBuffer.Size()); } + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(ImageFrame source) + { + Guard.NotNull(source, nameof(source)); + + this.UpdateMetadata(source.Metadata); + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 1a50229ca9..02403923d2 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -395,22 +395,42 @@ public sealed class Image : Image } /// - /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. + /// Switches the buffers used by the image and the pixel source meaning that the Image will + /// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer. /// - /// The pixel source. - internal void SwapOrCopyPixelsBuffersFrom(Image pixelSource) + /// The pixel source. + internal void SwapOrCopyPixelsBuffersFrom(Image source) { - Guard.NotNull(pixelSource, nameof(pixelSource)); + Guard.NotNull(source, nameof(source)); this.EnsureNotDisposed(); - ImageFrameCollection sourceFrames = pixelSource.Frames; + ImageFrameCollection sourceFrames = source.Frames; for (int i = 0; i < this.frames.Count; i++) { this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]); } - this.UpdateSize(pixelSource.Size); + this.UpdateSize(source.Size); + } + + /// + /// Copies the metadata from the source image. + /// + /// The metadata source. + internal void CopyMetadataFrom(Image source) + { + Guard.NotNull(source, nameof(source)); + + this.EnsureNotDisposed(); + + ImageFrameCollection sourceFrames = source.Frames; + for (int i = 0; i < this.frames.Count; i++) + { + this.frames[i].CopyMetadataFrom(sourceFrames[i]); + } + + this.UpdateMetadata(source.Metadata); } private static Size ValidateFramesAndGetSize(IEnumerable> frames) diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 9c0de1edbe..b24aa140fc 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Metadata; @@ -110,16 +111,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) { TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); - this.formatMetadata[key] = derivedMeta; + this.SetFormatMetadata(key, derivedMeta); return derivedMeta; } TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); - this.formatMetadata[key] = newMeta; + this.SetFormatMetadata(key, newMeta); return newMeta; } - internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) + /// + /// Sets the metadata value associated with the specified key. + /// + /// The type of format metadata. + /// The type of format frame metadata. + /// The key of the value to set. + /// The value to set. + public void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) where TFormatMetadata : class where TFormatFrameMetadata : class, IFormatFrameMetadata => this.formatMetadata[key] = value; @@ -143,4 +151,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable /// Synchronizes the profiles with the current metadata. /// internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + + /// + /// This method is called after a process has been applied to the image frame. + /// + /// The type of pixel format. + /// The source image frame. + /// The destination image frame. + internal void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + // Always updated using the full frame dimensions. + // Individual format frame metadata will update with sub region dimensions if appropriate. + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterFrameApply(source, destination); + } + } } diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 37557ba1dc..1961dbf192 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -230,6 +230,22 @@ public sealed class ImageMetadata : IDeepCloneable /// internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); + /// + /// This method is called after a process has been applied to the image. + /// + /// The type of pixel format. + /// The destination image. + internal void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel + { + this.ExifProfile?.SyncDimensions(destination.Width, destination.Height); + + foreach (KeyValuePair meta in this.formatMetadata) + { + meta.Value.AfterImageApply(destination); + } + } + internal PixelTypeInfo GetDecodedPixelTypeInfo() { // None found. Check if we have a decoded format to convert from. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 41d3c293b6..e91a69444d 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.PixelFormats; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -298,6 +299,19 @@ public sealed class ExifProfile : IDeepCloneable this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); } + internal void SyncDimensions(int width, int height) + { + if (this.TryGetValue(ExifTag.PixelXDimension, out _)) + { + this.SetValue(ExifTag.PixelXDimension, width); + } + + if (this.TryGetValue(ExifTag.PixelYDimension, out _)) + { + this.SetValue(ExifTag.PixelYDimension, height); + } + } + /// /// Synchronizes the profiles with the specified metadata. /// diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs index aa000a10e7..abe32e3882 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs @@ -48,7 +48,6 @@ public abstract class CloningImageProcessor : ICloningImageProcessor clone = this.CreateTarget(); this.CheckFrameCount(this.Source, clone); - Configuration configuration = this.Configuration; this.BeforeImageApply(clone); for (int i = 0; i < this.Source.Frames.Count; i++) @@ -77,9 +76,10 @@ public abstract class CloningImageProcessor : ICloningImageProcessor)this).CloneAndExecute(); - // We now need to move the pixel data/size data from the clone to the source. + // We now need to move the pixel data/size data and any metadata from the clone to the source. this.CheckFrameCount(this.Source, clone); this.Source.SwapOrCopyPixelsBuffersFrom(clone); + this.Source.CopyMetadataFrom(clone); } finally { @@ -157,7 +157,7 @@ public abstract class CloningImageProcessor : ICloningImageProcessor[source.Frames.Count]; + ImageFrame[] destinationFrames = new ImageFrame[source.Frames.Count]; for (int i = 0; i < destinationFrames.Length; i++) { destinationFrames[i] = new ImageFrame( diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs deleted file mode 100644 index 0bb4920f0f..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -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 UpdateDimensionalMetadata(Image image) - where TPixel : unmanaged, IPixel - { - ExifProfile? profile = image.Metadata.ExifProfile; - if (profile is null) - { - return; - } - - // Only set the value if it already exists. - if (profile.TryGetValue(ExifTag.PixelXDimension, out _)) - { - profile.SetValue(ExifTag.PixelXDimension, image.Width); - } - - if (profile.TryGetValue(ExifTag.PixelYDimension, out _)) - { - profile.SetValue(ExifTag.PixelYDimension, image.Height); - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs similarity index 80% rename from src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs index 0c2c29391b..bdfac00366 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs @@ -23,10 +23,17 @@ internal abstract class TransformProcessor : CloningImageProcessor + protected override void AfterFrameApply(ImageFrame source, ImageFrame destination) + { + base.AfterFrameApply(source, destination); + destination.Metadata.AfterFrameApply(source, destination); + } + /// protected override void AfterImageApply(Image destination) { - TransformProcessorHelpers.UpdateDimensionalMetadata(destination); base.AfterImageApply(destination); + destination.Metadata.AfterImageApply(destination); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 1972101164..0d59625ca7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff; @@ -292,6 +293,82 @@ public class TiffEncoderTests : TiffEncoderBaseTester Assert.Equal(expectedCompression, frameMetaData.Compression); } + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void TiffEncoder_EncodesMultiFrameMipMap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + + using MemoryStream memStream = new(); + image.SaveAsTiff(memStream); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + + Assert.Equal(image.Size, output.Size); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + for (int i = 0; i < image.Frames.Count; i++) + { + TiffFrameMetadata inputMetadata = image.Frames[i].Metadata.GetTiffMetadata(); + TiffFrameMetadata outputMetadata = output.Frames[i].Metadata.GetTiffMetadata(); + + Assert.Equal(inputMetadata.EncodingWidth, outputMetadata.EncodingWidth); + Assert.Equal(inputMetadata.EncodingHeight, outputMetadata.EncodingHeight); + } + } + + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void TiffEncoder_EncodesMultiFrameMipMap_WithScaling(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + Assert.Equal(7, image.Frames.Count); + + Size size = image.Size; + + List encodedDimensions = []; + foreach (ImageFrame frame in image.Frames) + { + TiffFrameMetadata metadata = frame.Metadata.GetTiffMetadata(); + encodedDimensions.Add(new Size(metadata.EncodingWidth, metadata.EncodingHeight)); + } + + const int scale = 2; + image.Mutate(x => x.Resize(image.Width / scale, image.Height / scale)); + + using MemoryStream memStream = new(); + image.SaveAsTiff(memStream); + + memStream.Position = 0; + using Image output = Image.Load(memStream); + + Assert.Equal(image.Size, output.Size); + Assert.Equal(image.Frames.Count, output.Frames.Count); + + // The encoded dimensions should automatically be scaled down by the + // horizontal and vertical scaling factors. + float ratioX = output.Width / (float)size.Width; + float ratioY = output.Height / (float)size.Height; + + for (int i = 0; i < image.Frames.Count; i++) + { + TiffFrameMetadata inputMetadata = image.Frames[i].Metadata.GetTiffMetadata(); + TiffFrameMetadata outputMetadata = output.Frames[i].Metadata.GetTiffMetadata(); + + int expectedWidth = (int)MathF.Ceiling(encodedDimensions[i].Width * ratioX); + int expectedHeight = (int)MathF.Ceiling(encodedDimensions[i].Height * ratioY); + + Assert.Equal(expectedWidth, inputMetadata.EncodingWidth); + Assert.Equal(expectedHeight, inputMetadata.EncodingHeight); + Assert.Equal(inputMetadata.EncodingWidth, outputMetadata.EncodingWidth); + Assert.Equal(inputMetadata.EncodingHeight, outputMetadata.EncodingHeight); + } + } + // This makes sure, that when decoding a planar tiff, the planar configuration is not carried over to the encoded image. [Theory] [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs deleted file mode 100644 index 21b92a01e8..0000000000 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Transforms; -using SixLabors.ImageSharp.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Tests.Processing.Transforms; - -[Trait("Category", "Processors")] -public class TransformsHelpersTest -{ - [Fact] - public void HelperCanChangeExifDataType() - { - int xy = 1; - - using (var img = new Image(xy, xy)) - { - var profile = new ExifProfile(); - img.Metadata.ExifProfile = profile; - profile.SetValue(ExifTag.PixelXDimension, xy + ushort.MaxValue); - profile.SetValue(ExifTag.PixelYDimension, xy + ushort.MaxValue); - - Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType); - Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType); - - TransformProcessorHelpers.UpdateDimensionalMetadata(img); - - Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType); - Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType); - } - } -} From c579547a4f0987d5defbf425be4301475c4a52d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 13 Aug 2024 22:29:47 +1000 Subject: [PATCH 4/4] Separate transform spaces --- .../Processing/AffineTransformBuilder.cs | 36 ++++++++-- .../Transforms/Linear/RotateProcessor.cs | 4 +- .../Transforms/Linear/SkewProcessor.cs | 4 +- .../Processors/Transforms/TransformUtils.cs | 70 ++++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 30 ++++++-- src/ImageSharp/Processing/TransformSpace.cs | 26 +++++++ .../Transforms/TransformBuilderTestBase.cs | 5 +- ...tPattern100x50_R(50)_S(1,1)_T(-20,-10).png | 4 +- ..._TestPattern100x50_R(50)_S(1,1)_T(0,0).png | 4 +- ...estPattern100x50_R(50)_S(1,1)_T(20,10).png | 4 +- ...ttern100x50_R(50)_S(1.1,1.3)_T(30,-20).png | 4 +- ...tPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png | 4 +- ...d_Rgba32_TestPattern96x96_R(50)_S(0.8).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- .../DrawImageTests/DrawTransformed.png | 4 +- ...sCSS_Rgba32_Solid290x154_(0,0,255,255).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...2_Solid30x30_(255,0,0,255)_Bottom-Both.png | 4 +- ...id30x30_(255,0,0,255)_Bottom-LeftOrTop.png | 4 +- ...x30_(255,0,0,255)_Bottom-RightOrBottom.png | 4 +- ...a32_Solid30x30_(255,0,0,255)_Left-Both.png | 4 +- ...olid30x30_(255,0,0,255)_Left-LeftOrTop.png | 4 +- ...30x30_(255,0,0,255)_Left-RightOrBottom.png | 4 +- ...32_Solid30x30_(255,0,0,255)_Right-Both.png | 4 +- ...lid30x30_(255,0,0,255)_Right-LeftOrTop.png | 4 +- ...0x30_(255,0,0,255)_Right-RightOrBottom.png | 4 +- ...ba32_Solid30x30_(255,0,0,255)_Top-Both.png | 4 +- ...Solid30x30_(255,0,0,255)_Top-LeftOrTop.png | 4 +- ...d30x30_(255,0,0,255)_Top-RightOrBottom.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- ...otate_WithAngle_TestPattern100x50_-170.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_-50.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_170.png | 4 +- .../Rotate_WithAngle_TestPattern100x50_50.png | 4 +- ...otate_WithAngle_TestPattern50x100_-170.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_-50.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_170.png | 4 +- .../Rotate_WithAngle_TestPattern50x100_50.png | 4 +- ...lType_Bgra32_TestPattern100x50_-20_-10.png | 4 +- ...xelType_Bgra32_TestPattern100x50_20_10.png | 4 +- ...elType_Rgb24_TestPattern100x50_-20_-10.png | 4 +- ...ixelType_Rgb24_TestPattern100x50_20_10.png | 4 +- 75 files changed, 262 insertions(+), 185 deletions(-) create mode 100644 src/ImageSharp/Processing/TransformSpace.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index e8c628ff12..4ac9546f39 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -13,6 +13,28 @@ public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public AffineTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the affine transform. + /// + public AffineTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the affine transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a rotation matrix using the given rotation angle in degrees /// and the image center point as rotation center. @@ -30,7 +52,7 @@ public class AffineTransformBuilder /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -66,7 +88,7 @@ public class AffineTransformBuilder /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -141,7 +163,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -150,7 +172,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -179,7 +201,7 @@ public class AffineTransformBuilder /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -188,7 +210,7 @@ public class AffineTransformBuilder /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -334,7 +356,7 @@ public class AffineTransformBuilder CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(matrix, size); + return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace); } private static void CheckDegenerate(Matrix3x2 matrix) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index aee7fd7816..0af2b268a1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -28,14 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), + TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 085d2bbec1..0bbc8e0f60 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -30,7 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), + TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel), sampler, sourceSize) { @@ -40,7 +40,7 @@ public sealed class SkewProcessor : AffineTransformProcessor // Helper constructor: private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 787e7fc3e3..62ea5e830d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -83,20 +83,22 @@ internal static class TransformUtils /// /// The amount of rotation, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace) + => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. /// /// The amount of rotation, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -104,10 +106,11 @@ internal static class TransformUtils /// The X angle, in degrees. /// The Y angle, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace) + => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -115,21 +118,25 @@ internal static class TransformUtils /// The X angle, in radians. /// The Y angle, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace); /// /// Gets the centered transform matrix based upon the source rectangle. /// /// The transformation matrix. /// The source image size. + /// + /// The to use when creating the centered matrix. + /// /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace) { - Size destinationSize = GetUnboundedTransformedSize(matrix, size); + Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace); // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. @@ -138,8 +145,10 @@ internal static class TransformUtils // The source size is provided using the coordinate space of the source image. // however the transform should always be applied in the pixel space. // To account for this we offset by the size - 1 to translate to the pixel space. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); + float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F; + + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -161,12 +170,6 @@ internal static class TransformUtils { Matrix4x4 matrix = Matrix4x4.Identity; - // The source size is provided using the Coordinate/Geometric space of the source image. - // However, the transform should always be applied in the Discrete/Pixel space to ensure - // that the transformation fully encompasses all pixels without clipping at the edges. - // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. - // size -= new Size(1, 1); - /* * SkMatrix is laid out in the following manner: * @@ -280,11 +283,10 @@ internal static class TransformUtils /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - public static Size GetTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, true); + /// The to use when calculating the size. + /// The . + public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, true); /// /// Returns the size relative to the source for the given transformation matrix. @@ -355,22 +357,22 @@ internal static class TransformUtils /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, false); + /// The to use when calculating the size. + /// The . + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, false); /// /// Returns the size relative to the source for the given transformation matrix. /// /// The transformation matrix. /// The source size. + /// The to use when calculating the size. /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -381,9 +383,13 @@ internal static class TransformUtils // Define an offset size to translate between coordinate space and pixel space. // Compute scaling factors from the matrix - float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) - float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) - SizeF offsetSize = new(scaleX, scaleY); + SizeF offsetSize = SizeF.Empty; + if (transformSpace == TransformSpace.Pixel) + { + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + offsetSize = new(scaleX, scaleY); + } // Subtract the offset size to translate to the pixel space. if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 06e6f0e71a..9027ee7266 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -13,6 +13,28 @@ public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public ProjectiveTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the projective transform. + /// + public ProjectiveTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the projective transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a matrix that performs a tapering projective transform. /// @@ -47,7 +69,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -81,7 +103,7 @@ public class ProjectiveTransformBuilder /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -165,7 +187,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -203,7 +225,7 @@ public class ProjectiveTransformBuilder /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. diff --git a/src/ImageSharp/Processing/TransformSpace.cs b/src/ImageSharp/Processing/TransformSpace.cs new file mode 100644 index 0000000000..bca676bd88 --- /dev/null +++ b/src/ImageSharp/Processing/TransformSpace.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing; + +/// +/// Represents the different spaces used in transformation operations. +/// +public enum TransformSpace +{ + /// + /// Coordinate space is a continuous, mathematical grid where objects and positions + /// are defined with precise, often fractional values. This space allows for fine-grained + /// transformations like scaling, rotation, and translation with high precision. + /// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries. + /// + Coordinate, + + /// + /// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen. + /// In this space, positions are defined by whole numbers, with no fractional values. + /// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3). + /// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen. + /// + Pixel +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 9d256efc1c..f046c2503c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Tests.Processing.Transforms; @@ -97,7 +98,7 @@ public abstract class TransformBuilderTestBase this.AppendRotationDegrees(builder, degrees); // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness - Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size); + Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -151,7 +152,7 @@ public abstract class TransformBuilderTestBase this.AppendSkewDegrees(builder, degreesX, degreesY); - Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size); + Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png index 2ba0560e65..480c07da48 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b926c8335eca5530d8704739cecae0799cc651139daedb1f88ac85b0ee1bd5d -size 9484 +oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729 +size 9747 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png index d01fcb4a49..227f546515 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:322c7e061f8565efdc19642e27353ec3073ee43d8c17fbef8c13be3bb60d11dc -size 10190 +oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db +size 10561 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png index 171080746b..b93742a858 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:544e7bac188d0869f98ec075fa0e73ab831e4dafe40c1520dce194df6a53c9b8 -size 12737 +oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0 +size 13227 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png index 07c65142a4..57c3b02ba7 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ccc08769974e4088702d2c95fd274af7e02095955953b424a6313d656a77735 -size 19974 +oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3 +size 20778 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png index 84fffa468f..b3bfc7ee51 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1179b300b35d526bab148833ab6240f1207b8ade36674b1f47cc5a2d47a084c -size 10603 +oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a +size 10943 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index 1f1d530517..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f666fe67ee4a1c7152fc6190affba95ea4cbd857d96bac0968e5f1fd89792d32 -size 13486 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 0ce7ad5625..63214687d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b05406a1d95f0709a7aaab7c1f57ba161b7907b76746f61788cfe527796a489 -size 4131 +oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735 +size 4165 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index e93934b48d..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be52d36cc8f616a781c8b1416ca0bf6207b9acd580e9c06e1ee5ad434d48ab38 -size 13481 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 2a68a381d4..ef37b3e2d6 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c99b8f0fb5d10a273a90946767f93ab6cd2dd1942f9829d695987db30dccfa -size 12488 +oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a +size 12559 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index 08f89da07e..93a0ce6c54 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af2c0201c59065a500ae985e9b7ca164e5bcb4ce2d8d8305103398830472e07c -size 14206 +oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2 +size 14223 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index 85d4871036..c2ca6bf57b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cef17988c4a3a667dede3dd86ed61d0507a84e5b846f52459683fd04e5a396a -size 17297 +oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272 +size 17927 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 347cbf089f..ade9a1ccd8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9699a81572c03c2bc47d8bbdd1d64df26f87df3d4ad59fb6f164f6e82786d91d -size 18853 +oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc +size 18945 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 69fe0b1355..cf04e20363 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fb1f59c5393debdff9bd4b7a6c222b7a0686e6d5ef24363e3d5c94ba9b5bc27 -size 20725 +oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af +size 20844 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 0fa8cf0c06..6be0fc0ff8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ffa8ca6a60de9fe26a191edc2127886c61c072c1aa2b91fe3125512fe40e1b3 -size 13848 +oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b +size 13857 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 36e409ecb9..0064e973ff 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abf1d0f323795c0aaff0ff8b488d9866c5b2f7c64aad83701cb1f60e22668b0e -size 4161 +oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f +size 4154 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index f8f102e4c8..5dd0c52255 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0f3c41248138bd501ae844e5b54fb9f49e5d22bab9b2ef0a0654034708b99f -size 14027 +oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07 +size 13991 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index fc46cad7c0..a6e120e904 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a3f839d64984b9fda4125c6643f4699add6f95373a2194c5726ed3740565a47 -size 13725 +oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b +size 13744 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 58e879d4e3..d32c11d44c 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ac143bc73612cecfffbec049a7b68234e7bf7581e680c3f996a977c6d671cc1 -size 14865 +oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca +size 14889 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index fa25146d9c..72782b0b99 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb9dab20d5a03c0adde05a9b741d9e1b0fb8c3d79054a8bc5788de496e5c7f8 -size 12420 +oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f +size 12374 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index dc8ea690c2..6cedab729b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f56ee78cc2fd698ac8ea84912648f0b49d4b4d66b439f6976447c56a44c2998 -size 16909 +oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328 +size 17027 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index 4b9953b670..7368a3b007 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd3b29b530e221618f65cd5e493b21fe3c27804fde7664636b7bb002f72abbb2 -size 3663 +oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921 +size 854 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 5f4911e47c..da66b26768 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a35757fef08a6fd9b37e719d5be7a82d5ff79f0395e082f697d9ebe9c7f03cc8 -size 5748 +oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a +size 5520 diff --git a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png index 17238cf2f7..7e693a5839 100644 --- a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png +++ b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00836a98742d177a2376af32d8d858fcf9f26a4da5a311dd5faf5cd80f233c0b -size 184397 +oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d +size 184124 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png index e7ed4a95f5..f2e87f8509 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d40d0715f695dc55dc2e435cab2c999b657b59ead2e0cc8a95edf7cea7782750 -size 6842 +oid sha256:455b17bc432490435c2453424d17b92b77d036dcbc2b2ab06b960a398bd3136b +size 11119 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 5518047e4d..f9aa1ffe03 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d502e2d6b0eb5f12ed3262eb4654927cc937574ae1de61a1d89f6672592017 -size 11828 +oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c +size 2757 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 29014117e3..2f9109ba38 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f1e69bfd1f4e9479839d4bddfb2ecc68ff8cda9b15055427406691df94a16db -size 9583 +oid sha256:57698b6666029a55edf8bd35a7ba96f68d224988cf01308a3af1c6606ae9d0b1 +size 10174 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index d417cad9c1..7dfec78983 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd1d0350ca49ff1726c2a20769693698168497944413c653da6edcb6bc9a39e5 -size 17344 +oid sha256:fc7c9da04142a679887c714c43f1838eba0092a869140d234fce3412673207c6 +size 13575 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index cfd3cd48ef..6e3b97f2df 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dc8ed5f721d2d8ae81f1b70308511737b0e649a6b05a011342efce29fa5b1cb -size 23022 +oid sha256:d8b973f41f8afa39b94c71b307b7eb393953e2d083d56e1f0e8f43d6ab1f342a +size 16821 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 2578c537e8..6986c03912 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9514b25bbc875eb00b5c0dc4241f973d466075914a9ec4c4b64ba251323eb5a -size 22321 +oid sha256:122c1501e09516244f0db36e1cca373ff68514a18e84f57ed3072d52d6112e36 +size 17022 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 1f6609c6f0..76b53fabfb 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09138a5ec45480a72370eba3015e7dee33360712745cb2f00e36a5994c1e48a7 -size 24090 +oid sha256:12181516bce69c9302f15bba928fd530796449599cb9744a2411cc796788ee3b +size 18066 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 7e4f16695a..ae4242a42b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed80155afab90710ebab4babbbb787402ceac4f9dee1ff270361e5e8e495c73a -size 13867 +oid sha256:1eb5accc5ada5b963ecef6ac15bfb1845f481e51aef63e06a522ea73bbeab945 +size 11194 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 3297f99e21..efb6a2deed 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96fa0ebee48290be808d5c7dec561bbcab49e5222667e175ffff01b3a175c586 -size 2308 +oid sha256:0418f0ea38ec19b407f2b5046d7ff0ed207189ad71db1e50e82c419d83620543 +size 2759 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index 48c51e7c3c..976be43a3b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c3ce34e6bbd8e35ec979acc28510342dadf0bae3d91a9e1b8291cdff638465 -size 13873 +oid sha256:1233a9ab2c4b0b17b0248c3d40050c466330c22095287dfbdb8bf7dfbda4ff1f +size 11212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index 379b25ca0e..04fb2e87e0 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87676e4bf55e71815acc46bdeb3384d659fe6cb3ecc7b730fd9ecc8be00d181f -size 13847 +oid sha256:e2912d4e42c7b76d9ff48a49921d6472e351662597d69b88bc3708683c7933e3 +size 11221 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 4bcd2dc604..b35d68aaf8 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34a62df6b4e9756a7dfef965b669b522e2338b0c2e267d35e4a37fcd9d8a55c3 -size 14818 +oid sha256:51b05c38647e0c1d88cc722e4109a882305073a065d2a27ccd3bee82f727127d +size 11775 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index 009fc4fa60..64b9c6aba4 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60cf2a3fae30bee16131092590e2a8fe139070b6cf98c3b98698e34ededb711f -size 11996 +oid sha256:b260e816b23a43d7efb7507897ba2f5dbb6a596dd67a5abd4c9a0c005e926ee0 +size 9748 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index 568625cd6c..29b95bf525 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2140365f533762cfc4411105be04a13f7d85739d955152c3e1951c2c69c7d67 -size 19934 +oid sha256:50b03d627bb53048f5e56380500f116da4c503f5bb6a1b1d3c0d67ee4256d8f6 +size 15977 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png index 92a99f0360..54dca26397 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57bd912acfd243882163775196e0b02c7d0374e81319cb29902cd560b5c6053 -size 394 +oid sha256:96454548849147d7c7f0ca507c8521a7d5eaa7771f9f383cc836858870b52c57 +size 280 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png index cc89f5114b..41f94c9c7b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8827b81929abd9000fcfd3ce747b82fec4c778bcd29bb12abaaed5f8b0dfc945 -size 228 +oid sha256:e94d224fdb284b6f1ba21b8caa66174edd7e6a3027f9dd03f4757e08296e6508 +size 212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png index 61128d7b83..49cd1c8375 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5819f0deb160a7c7bf090919a010a2f8a3c074b3b718f56fe56f1f6eae1fcdcd -size 227 +oid sha256:d1162be9fa1f31bee8d3cba05c1422a1945621a412be11cce13d376efd5c679c +size 173 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png index 846fe440ef..59f928178a 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1381429147add3a9d3342bb2d618a47a1a5db997a384582a288705f68f5f937a -size 343 +oid sha256:0ed262e9b885af773a4a40a4506e678630670e208bf7f9ec10307e943b166bed +size 258 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png index 7b0692ffe2..57ee3dc2ff 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e59892f86d307e28457ebd621fec84b8ab839cdc11afe38e978420c4173058e4 -size 236 +oid sha256:3a24f2cfc225d01294b8bbc5ca7d7f1738fb0b79217046eb9edf04e4c4c01851 +size 201 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png index 4c0d43feaf..7e47f43ff9 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d25a9407bb5a3c8ba7126c9082dfbc5f29c5b52fc46e1a21e1c8968ac9f1c11 -size 230 +oid sha256:938186fb3d0f468176988a9530efd22e66241a1361fff027005ec8a8ae323ff3 +size 197 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png index a832badcb0..0f756e7813 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:398d1ca71075c541fdb88eb63f5889edea2259fdd0df643bed77e64baa246ceb -size 317 +oid sha256:4bc4b8ea7e7f10676d8de612fe6bc5144e100b95ff3fe7a1e3d4066a7684ce4d +size 239 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png index bdef30001b..b2d420886b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62546cd019fa11a43d254936c12b5adcbe8eebe6ae012bc1024949df6b28303f -size 220 +oid sha256:345337f7dffa48d95251503ee2ae8e91db98b5cbe06b579d73c38a018c781544 +size 182 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png index 3012731317..4f0ad9d045 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e444aff3db9c5a1149de73f3b0ed18e48c8dc372be13c3555dcda0ec4ad893 -size 221 +oid sha256:de4e2b71dade9dfb750a2c614a684963d6962958db79145c87fd23d9f0f8c005 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png index dd6b926db4..78bdb8bbbe 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bdfa8eb65b5e54b9ccffb11acb2a3b7a5a434f1f22a2cdf792d892fd411f711 -size 399 +oid sha256:8d8b651663366e7543211635f337c229e2f88f1142886ea3a9b69587daaada97 +size 288 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png index fb98036ec4..7015a05571 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11a1af248350450454ba1cb415357e58f905de402a921b213a305990e8f57c3 -size 245 +oid sha256:8ab8df31f1716c05bb8687f79c7d1154f6cc6f65e3917abe60ecc42d0df173dc +size 215 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png index 0e4ef4c427..67a765e8db 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9cffb16fbff28901a3daf391469d32bf36f3c293aa870d169967f17611dea92 -size 241 +oid sha256:1a1671da9ea7702a37a866fabfb3ca0d746233ee108594198f23cb563af43ae6 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index e8efa8a980..9d7dc06c95 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39c25539c3c9b8926bf65c041df693a60617bbe8653bb72357bde5ab6342c59c -size 3618 +oid sha256:f5aef9cd3b8bfd9859e5d2401e82f89d89407ab2834b09c43f0a3229c735e92b +size 724 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 99a74e400a..4b2bb99d96 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b1fc95fdf07c7443147205afffb157aa82f94818cfbb833a615c42f584fbda0 -size 5070 +oid sha256:1e7dedec16ccd66543a1d44052a104957ba62099ba2f2ccc72285c233c2ae3fa +size 4411 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png index a9289abd0d..65bb77977b 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801067dfb19b2a9a1fbd14b771e780b475c21f3ccdc1e709bbc20d62061ad1d1 -size 8782 +oid sha256:3594547265b23603b1a76ff6bc6f0eab4af55d6e0070e53356123dfc7ae256f8 +size 9034 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png index 43fcd5df5e..7c54b1b074 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccbdd3dcdbc888923e85495fbd7837478cca813be6ecece63ee645bdf39d436f -size 10325 +oid sha256:5ae9ef073f3338b71d2a40fcf2e89d9b6ab62204d6de9b6a1f75f4705ee197f0 +size 10704 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png index 9a7f9866da..b6e930224e 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60d1be2ffa6d50f97561b92efe8d0c0337f9c121582e38c9ab9af75be8eed32d -size 8539 +oid sha256:994dda7da034595aa77d107652bea06c86077d24ef8a6883b18f1f509bb19928 +size 8906 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png index 1d27e23958..2f3f0f17fe 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75fb59ea2947efb1abf73cd824e54b6e8f6271343bd2fdadb005b29476988921 -size 9423 +oid sha256:29c5f48f1ece0b12854b4c44fba84fdfc9ac5751cdf564a32478dcdaed43b2a4 +size 9798 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png index 628d0c889c..5242a9d985 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2d94791af40e001fc44b23eeecac7a606492c747b22ede0cdc7069ef121cb8 -size 11193 +oid sha256:c7de58474c3f386c4ec31a9088d561a513f82c08d1157132d735169b847b9680 +size 11579 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png index 0e927cfbd0..2af9d2fc27 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:016de6e82b6fb03fd55168ea7fc12ab245d0e0387ca7c32d3ef1158a85a8facd -size 9330 +oid sha256:3ef9b7051d7a5733dfe2534fddefdc28dfbc49d087355f46c4d945b04f0e3936 +size 9672 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png index ac4d473624..83c02764fa 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9219cc118fe7195b730cbe2e6407cde54e6f4c7930a71b7418bc7c273aa4120c -size 11050 +oid sha256:825770c9b2e9f265d834eab6b40604df5508bf9bc5b4f82f5d3effd6d5a26935 +size 11434 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png index 147f9c9897..d6dba3f889 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b243340f372220033b349a96cbd7ecd732395fa15e4d1ed62048d2031c42794 -size 8398 +oid sha256:1e283463b0f450dd72cf303acccf3dd1ff7a31fe401ff0f288d67c4baefca240 +size 8742 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png index d1252cb2c4..76bb244d52 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd2745aba2f09bb4a09e881339fe62774ce5658902aa9d83b3a1e0718260084 -size 8694 +oid sha256:485d9d9ef955a04af43d17e6bc3952e9bf65a9752b6cf8ba9cbbe8f772f05a18 +size 8995 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png index 235e95d8fc..c1c1d814fd 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281bd00550ab1cf9b05011750e6de330cdd42a8644ecf3b7c176bd5c6e94c59b -size 6098 +oid sha256:d3d749ac365764051ea16bc39d1aff84c06faf282359805b58bb97c9eed7f0bb +size 6400 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png index 3b63f58f36..27608881ed 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e755ed61d7eab2c297f4b6592366b54ac801cbdb3d920741dfdd04dfaf73f9b9 -size 6086 +oid sha256:8d82f2a15502b0a29aa4df1077ec90c88f9211f283fdc0edd7b059ed9b387441 +size 6334