Browse Source

Merge remote-tracking branch 'upstream/main' into heic-support

pull/2633/head
Ynse Hoornenborg 2 years ago
parent
commit
d93b89c3d9
  1. 32
      src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
  2. 33
      src/ImageSharp/Formats/AnimatedImageMetadata.cs
  3. 6
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  4. 4
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  5. 20
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  6. 32
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  7. 5
      src/ImageSharp/Formats/DecoderOptions.cs
  8. 2
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  9. 37
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  10. 6
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  11. 11
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  12. 8
      src/ImageSharp/Formats/IFormatMetadata.cs
  13. 2
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  14. 20
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  15. 20
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  16. 6
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  17. 6
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  18. 10
      src/ImageSharp/Formats/Png/PngChunk.cs
  19. 30
      src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs
  20. 12
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  21. 5
      src/ImageSharp/Formats/Png/PngDecoderOptions.cs
  22. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  23. 7
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  24. 6
      src/ImageSharp/Formats/Png/PngMetadata.cs
  25. 6
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  26. 30
      src/ImageSharp/Formats/SegmentIntegrityHandling.cs
  27. 6
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  28. 168
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  29. 18
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  30. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  31. 155
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  32. 6
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  33. 27
      src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
  34. 19
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  35. 28
      src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
  36. 21
      src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
  37. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
  38. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
  39. 7
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  40. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
  41. 4
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  42. 8
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  43. 6
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  44. 10
      src/ImageSharp/Image.cs
  45. 32
      src/ImageSharp/ImageFrame.cs
  46. 2
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  47. 24
      src/ImageSharp/ImageFrame{TPixel}.cs
  48. 36
      src/ImageSharp/Image{TPixel}.cs
  49. 33
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  50. 16
      src/ImageSharp/Metadata/ImageMetadata.cs
  51. 14
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  52. 2
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs
  53. 2
      src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
  54. 60
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  55. 6
      src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
  56. 4
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  57. 2
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  58. 2
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  59. 2
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  60. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
  61. 7
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
  62. 7
      src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
  63. 39
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs
  64. 9
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
  65. 290
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  66. 61
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  67. 26
      src/ImageSharp/Processing/TransformSpace.cs
  68. 18
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  69. 11
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  70. 1
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs
  71. 77
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  72. 18
      tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs
  73. 10
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  74. 5
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  75. 35
      tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs
  76. 3
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs
  77. 3
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs
  78. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png
  79. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png
  80. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png
  81. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png
  82. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png
  83. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png
  84. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  85. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png
  86. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png
  87. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png
  88. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png
  89. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png
  90. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png
  91. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png
  92. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png
  93. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png
  94. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png
  95. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png
  96. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png
  97. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png
  98. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png
  99. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png
  100. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png

32
src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageFrameMetadata
{
/// <summary>
/// Gets or sets the frame color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the duration of the frame.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets the frame alpha blending mode.
/// </summary>
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the frame disposal mode.
/// </summary>
public FrameDisposalMode DisposalMode { get; set; }
}

33
src/ImageSharp/Formats/AnimatedImageMetadata.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageMetadata
{
/// <summary>
/// Gets or sets the shared color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the shared color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// 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 <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
/// </remarks>
/// </summary>
public ushort RepeatCount { get; set; }
}

6
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
/// <inheritdoc/> /// <inheritdoc/>
public BmpMetadata DeepClone() => new(this); public BmpMetadata DeepClone() => new(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
} }

4
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore
curMetadata.Compression = compression; curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable; curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
curMetadata.HotspotX = curFrameMetadata.HotspotX;
curMetadata.HotspotY = curFrameMetadata.HotspotY;
} }
} }
} }

20
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
EncodingHeight = this.EncodingHeight EncodingHeight = this.EncodingHeight
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
ColorType = color 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));
}
} }

32
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
private CurMetadata(CurMetadata other) private CurMetadata(CurMetadata other)
{ {
this.Compression = other.Compression; this.Compression = other.Compression;
this.HotspotX = other.HotspotX;
this.HotspotY = other.HotspotY;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel; this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0) if (other.ColorTable?.Length > 0)
@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
/// </summary> /// </summary>
public IconFrameCompression Compression { get; set; } public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
/// </summary>
public ushort HotspotY { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
ColorTable = this.ColorTable ColorTable = this.ColorTable
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

5
src/ImageSharp/Formats/DecoderOptions.cs

@ -55,5 +55,10 @@ public sealed class DecoderOptions
/// </summary> /// </summary>
public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); } public uint MaxFrames { get => this.maxFrames; init => this.maxFrames = Math.Clamp(value, 1, int.MaxValue); }
/// <summary>
/// Gets the segment error handling strategy to use during decoding.
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
internal void SetConfiguration(Configuration configuration) => this.configuration = configuration; internal void SetConfiguration(Configuration configuration) => this.configuration = configuration;
} }

2
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -209,7 +209,7 @@ internal sealed class GifEncoderCore
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {

37
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/> /// <inheritdoc/>
public GifFrameMetadata DeepClone() => new(this); 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<Color> 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,
};
}
} }

6
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

11
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns> /// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns>
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
/// <summary>
/// This method is called after a process has been applied to the image frame.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
} }
/// <summary> /// <summary>

8
src/ImageSharp/Formats/IFormatMetadata.cs

@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <returns>The <see cref="FormatConnectingMetadata"/>.</returns> /// <returns>The <see cref="FormatConnectingMetadata"/>.</returns>
FormatConnectingMetadata ToFormatConnectingMetadata(); FormatConnectingMetadata ToFormatConnectingMetadata();
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore
curMetadata.Compression = compression; curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable; curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
} }
} }
} }

20
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
EncodingHeight = this.EncodingHeight EncodingHeight = this.EncodingHeight
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
ColorType = color 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));
}
} }

20
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
private IcoMetadata(IcoMetadata other) private IcoMetadata(IcoMetadata other)
{ {
this.Compression = other.Compression; this.Compression = other.Compression;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel; this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0) if (other.ColorTable?.Length > 0)
@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
/// </summary> /// </summary>
public IconFrameCompression Compression { get; set; } public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
ColorTable = this.ColorTable ColorTable = this.ColorTable
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
Quality = this.Quality, Quality = this.Quality,
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Pbm/PbmMetadata.cs

@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
PixelTypeInfo = this.GetPixelTypeInfo(), PixelTypeInfo = this.GetPixelTypeInfo(),
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

10
src/ImageSharp/Formats/Png/PngChunk.cs

@ -41,13 +41,13 @@ internal readonly struct PngChunk
/// <summary> /// <summary>
/// Gets a value indicating whether the given chunk is critical to decoding /// Gets a value indicating whether the given chunk is critical to decoding
/// </summary> /// </summary>
/// <param name="handling">The chunk CRC handling behavior.</param> /// <param name="handling">The segment handling behavior.</param>
public bool IsCritical(PngCrcChunkHandling handling) public bool IsCritical(SegmentIntegrityHandling handling)
=> handling switch => handling switch
{ {
PngCrcChunkHandling.IgnoreNone => true, SegmentIntegrityHandling.IgnoreNone => true,
PngCrcChunkHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData,
PngCrcChunkHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette,
_ => false, _ => false,
}; };
} }

30
src/ImageSharp/Formats/Png/PngCrcChunkHandling.cs

@ -1,30 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
/// </summary>
public enum PngCrcChunkHandling
{
/// <summary>
/// Do not ignore any CRC chunk errors.
/// </summary>
IgnoreNone,
/// <summary>
/// Ignore CRC errors in non critical chunks.
/// </summary>
IgnoreNonCritical,
/// <summary>
/// Ignore CRC errors in data chunks.
/// </summary>
IgnoreData,
/// <summary>
/// Ignore CRC errors in all chunks.
/// </summary>
IgnoreAll
}

12
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -119,7 +119,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// <summary> /// <summary>
/// How to handle CRC errors. /// How to handle CRC errors.
/// </summary> /// </summary>
private readonly PngCrcChunkHandling pngCrcChunkHandling; private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary> /// <summary>
/// A reusable Crc32 hashing instance. /// A reusable Crc32 hashing instance.
@ -142,7 +142,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
this.maxFrames = options.GeneralOptions.MaxFrames; this.maxFrames = options.GeneralOptions.MaxFrames;
this.skipMetadata = options.GeneralOptions.SkipMetadata; this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling; this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
} }
@ -154,7 +154,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
this.skipMetadata = true; this.skipMetadata = true;
this.configuration = options.GeneralOptions.Configuration; this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling; this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes; this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
} }
@ -833,7 +833,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break; break;
default: default:
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll) if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{ {
goto EXIT; goto EXIT;
} }
@ -939,7 +939,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break; break;
default: default:
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll) if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{ {
goto EXIT; goto EXIT;
} }
@ -1927,7 +1927,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
private void ValidateChunk(in PngChunk chunk, Span<byte> buffer) private void ValidateChunk(in PngChunk chunk, Span<byte> buffer)
{ {
uint inputCrc = this.ReadChunkCrc(buffer); uint inputCrc = this.ReadChunkCrc(buffer);
if (chunk.IsCritical(this.pngCrcChunkHandling)) if (chunk.IsCritical(this.segmentIntegrityHandling))
{ {
Span<byte> chunkType = stackalloc byte[4]; Span<byte> chunkType = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type);

5
src/ImageSharp/Formats/Png/PngDecoderOptions.cs

@ -11,11 +11,6 @@ public sealed class PngDecoderOptions : ISpecializedDecoderOptions
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
/// <summary>
/// Gets a value indicating how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
/// </summary>
public PngCrcChunkHandling PngCrcChunkHandling { get; init; } = PngCrcChunkHandling.IgnoreNonCritical;
/// <summary> /// <summary>
/// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed. /// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed.
/// Defaults to 8MB /// Defaults to 8MB

2
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -231,7 +231,7 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{ {

7
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

30
src/ImageSharp/Formats/SegmentIntegrityHandling.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Specifies how to handle validation of errors in different segments of encoded image files.
/// </summary>
public enum SegmentIntegrityHandling
{
/// <summary>
/// Do not ignore any errors.
/// </summary>
IgnoreNone,
/// <summary>
/// Ignore errors in non-critical segments of the encoded image.
/// </summary>
IgnoreNonCritical,
/// <summary>
/// Ignore errors in data segments (e.g., image data, metadata).
/// </summary>
IgnoreData,
/// <summary>
/// Ignore errors in all segments.
/// </summary>
IgnoreAll
}

6
src/ImageSharp/Formats/Tga/TgaMetadata.cs

@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata<TgaMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

168
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore
this.byteOrder = reader.ByteOrder; this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff; this.isBigTiff = reader.IsBigTiff;
Size? size = null;
uint frameCount = 0; uint frameCount = 0;
foreach (ExifProfile ifd in directories) foreach (ExifProfile ifd in directories)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken); ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, size, cancellationToken);
if (!size.HasValue)
{
size = frame.Size;
}
frames.Add(frame); frames.Add(frame);
framesMetadata.Add(frame.Metadata); 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); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
// TODO: Tiff frames can have different sizes.
ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames)
{
if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
}
return new Image<TPixel>(this.configuration, metadata, frames); return new Image<TPixel>(this.configuration, metadata, frames);
} }
catch catch
@ -215,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore
IList<ExifProfile> directories = reader.Read(); IList<ExifProfile> directories = reader.Read();
List<ImageFrameMetadata> framesMetadata = []; List<ImageFrameMetadata> 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); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
int height = GetImageHeight(rootFrameExifProfile);
return new ImageInfo(new(width, height), metadata, framesMetadata); return new ImageInfo(new(width, height), metadata, framesMetadata);
} }
@ -235,31 +235,46 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="size">The previously determined root frame size if decoded.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
/// <returns>The tiff frame.</returns> /// <returns>The tiff frame.</returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken) private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, Size? size, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags);
bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata()); bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata);
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)
{
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);
}
int width = GetImageWidth(tags); ImageFrame<TPixel> frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
int height = GetImageHeight(tags);
ImageFrame<TPixel> frame = new(this.configuration, width, height, imageFrameMetaData);
if (isTiled) if (isTiled)
{ {
this.DecodeImageWithTiles(tags, frame, cancellationToken); this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
} }
else else
{ {
this.DecodeImageWithStrips(tags, frame, cancellationToken); this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
} }
return frame; return frame;
} }
private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags) private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
{ {
ImageFrameMetadata imageFrameMetaData = new(); ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata) if (!this.skipMetadata)
@ -267,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore
imageFrameMetaData.ExifProfile = tags; imageFrameMetaData.ExifProfile = tags;
} }
TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags); TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags);
imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata);
return imageFrameMetaData; return (imageFrameMetaData, tiffMetadata);
} }
/// <summary> /// <summary>
@ -278,8 +294,10 @@ internal class TiffDecoderCore : ImageDecoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param> /// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken) private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int rowsPerStrip; int rowsPerStrip;
@ -302,6 +320,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
this.DecodeStripsPlanar( this.DecodeStripsPlanar(
frame, frame,
width,
height,
rowsPerStrip, rowsPerStrip,
stripOffsets, stripOffsets,
stripByteCounts, stripByteCounts,
@ -311,6 +331,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
this.DecodeStripsChunky( this.DecodeStripsChunky(
frame, frame,
width,
height,
rowsPerStrip, rowsPerStrip,
stripOffsets, stripOffsets,
stripByteCounts, stripByteCounts,
@ -324,13 +346,13 @@ internal class TiffDecoderCore : ImageDecoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param> /// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken) private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
int width = pixels.Width;
int height = pixels.Height;
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueWidth)) if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueWidth))
{ {
@ -384,11 +406,20 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param> /// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The number of rows per strip of data.</param> /// <param name="rowsPerStrip">The number of rows per strip of data.</param>
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param> /// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param> /// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsPlanar<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int stripsPerPixel = this.BitsPerSample.Channels; int stripsPerPixel = this.BitsPerSample.Channels;
@ -403,18 +434,18 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) 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<byte>(uncompressedStripSize); stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize);
} }
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel); using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>(); TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
for (int i = 0; i < stripsPerPlane; i++) for (int i = 0; i < stripsPerPlane; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); 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; int stripIndex = i;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
@ -430,7 +461,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripIndex += stripsPerPlane; stripIndex += stripsPerPlane;
} }
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
} }
} }
finally finally
@ -447,39 +478,48 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param> /// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The rows per strip.</param> /// <param name="rowsPerStrip">The rows per strip.</param>
/// <param name="stripOffsets">The strip offsets.</param> /// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param> /// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsChunky<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) 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; int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean); using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean);
Span<byte> stripBufferSpan = stripBuffer.GetSpan(); Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel); using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>(); TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip ? rowsPerStrip
: frame.Height % rowsPerStrip; : height % rowsPerStrip;
int top = rowsPerStrip * stripIndex; 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). // Make sure we ignore any strips that are not needed for the image (if too many are present).
break; break;
@ -493,7 +533,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripBufferSpan, stripBufferSpan,
cancellationToken); cancellationToken);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
} }
} }
@ -790,38 +830,6 @@ internal class TiffDecoderCore : ImageDecoderCore
return bytesPerRow * height; return bytesPerRow * height;
} }
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number> 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;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number> height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
} }

18
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore
long ifdOffset) long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// 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( using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
compression, compression,
writer.BaseStream, writer.BaseStream,
this.memoryAllocator, this.memoryAllocator,
frame.Width, width,
(int)bitsPerPixel, (int)bitsPerPixel,
this.compressionLevel, this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore
using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create( using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation, this.PhotometricInterpretation,
frame, frame,
encodingSize,
this.quantizer, this.quantizer,
this.pixelSamplingStrategy, this.pixelSamplingStrategy,
this.memoryAllocator, this.memoryAllocator,
@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore
entriesCollector, entriesCollector,
(int)bitsPerPixel); (int)bitsPerPixel);
int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip); colorWriter.Write(compressor, rowsPerStrip);
@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore
// Write the metadata for the frame // Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata); entriesCollector.ProcessMetadata(frame, this.skipMetadata);
entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata);
entriesCollector.ProcessImageFormat(this); entriesCollector.ProcessImageFormat(this);
if (writer.Position % 2 != 0) if (writer.Position % 2 != 0)

10
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(ImageFrame frame, bool skipMetadata) public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata); => new MetadataProcessor(this).Process(frame, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata); => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata);
public void ProcessImageFormat(TiffEncoderCore encoder) public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(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) this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth)
{ {
Value = (uint)frame.Width Value = (uint)encodingSize.Width
}); });
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength)
{ {
Value = (uint)frame.Height Value = (uint)encodingSize.Height
}); });
this.ProcessResolution(imageMetadata); this.ProcessResolution(imageMetadata);

155
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
this.PhotometricInterpretation = other.PhotometricInterpretation; this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor; this.Predictor = other.Predictor;
this.InkSet = other.InkSet; this.InkSet = other.InkSet;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
} }
/// <summary> /// <summary>
@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
/// </summary> /// </summary>
public TiffInkSet? InkSet { get; set; } public TiffInkSet? InkSet { get; set; }
/// <summary>
/// Gets or sets the encoding width.
/// </summary>
public int EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height.
/// </summary>
public int EncodingHeight { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) 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;
}
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new(); => new()
{
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
/// </summary> /// </summary>
/// <param name="meta">The tiff frame meta data.</param> /// <param name="meta">The tiff frame meta data.</param>
/// <param name="profile">The Exif profile containing tiff frame directory tags.</param> /// <param name="profile">The Exif profile containing tiff frame directory tags.</param>
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<ushort[]>? bitsPerSampleValue)
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{ {
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue) meta.Compression = (TiffCompression)compressionValue.Value;
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{
meta.Compression = (TiffCompression)compressionValue.Value;
}
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? 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);
} }
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? 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);
}
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number>? 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;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number>? height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
} }
} }

6
src/ImageSharp/Formats/Tiff/TiffMetadata.cs

@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata<TiffMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

27
src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs

@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
{ {
private bool isDisposed; private bool isDisposed;
protected TiffBaseColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) protected TiffBaseColorWriter(
ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
{ {
this.Width = encodingSize.Width;
this.Height = encodingSize.Height;
this.Image = image; this.Image = image;
this.MemoryAllocator = memoryAllocator; this.MemoryAllocator = memoryAllocator;
this.Configuration = configuration; this.Configuration = configuration;
@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
/// </summary> /// </summary>
public abstract int BitsPerPixel { get; } public abstract int BitsPerPixel { get; }
/// <summary>
/// Gets the width of the portion of the image to be encoded.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the portion of the image to be encoded.
/// </summary>
public int Height { get; }
/// <summary> /// <summary>
/// Gets the bytes per row. /// Gets the bytes per row.
/// </summary> /// </summary>
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<TPixel> Image { get; } protected ImageFrame<TPixel> Image { get; }
@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) 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"); 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[] stripOffsets = new uint[stripsCount];
uint[] stripByteCounts = new uint[stripsCount]; uint[] stripByteCounts = new uint[stripsCount];
int stripIndex = 0; int stripIndex = 0;
compressor.Initialize(rowsPerStrip); 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; 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); this.EncodeStrip(y, height, compressor);
long endOffset = compressor.Output.Position; long endOffset = compressor.Output.Position;

19
src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs

@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
private IMemoryOwner<byte> bitStrip; private IMemoryOwner<byte> bitStrip;
public TiffBiColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffBiColorWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
// Convert image to black and white. // Convert image to black and white.
this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), new[] { image.Clone() }); this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), [image.Clone()]);
this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg));
} }
@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) 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. // Special case for T4BitCompressor.
int stripPixels = width * height; int stripPixels = width * height;
@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
int bitIndex = 0; int bitIndex = 0;
int byteIndex = 0; int byteIndex = 0;
Span<byte> outputRow = rows[(outputRowIdx * this.BytesPerRow)..]; Span<byte> outputRow = rows[(outputRowIdx * this.BytesPerRow)..];
Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row); Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width];
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); PixelOperations<TPixel>.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; int shift = 7 - bitIndex;
if (pixelAsGraySpan[x] == 255) if (pixelAsGraySpan[x] == 255)

28
src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs

@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory
public static TiffBaseColorWriter<TPixel> Create<TPixel>( public static TiffBaseColorWriter<TPixel> Create<TPixel>(
TiffPhotometricInterpretation? photometricInterpretation, TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame<TPixel> image, ImageFrame<TPixel> image,
Size encodingSize,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel) int bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => photometricInterpretation switch
switch (photometricInterpretation)
{ {
case TiffPhotometricInterpretation.PaletteColor: TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter<TPixel>(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel),
return new TiffPaletteWriter<TPixel>(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch
case TiffPhotometricInterpretation.BlackIsZero: {
case TiffPhotometricInterpretation.WhiteIsZero: 1 => new TiffBiColorWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
return bitsPerPixel switch 16 => new TiffGrayL16Writer<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
{ _ => new TiffGrayWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector)
1 => new TiffBiColorWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector), },
16 => new TiffGrayL16Writer<TPixel>(image, memoryAllocator, configuration, entriesCollector), _ => new TiffRgbWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
_ => new TiffGrayWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector) };
};
default:
return new TiffRgbWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector);
}
}
} }

21
src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs

@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
/// <summary> /// <summary>
/// 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 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).
/// </summary> /// </summary>
/// <typeparam name="TPixel">The tpe of pixel format.</typeparam>
internal abstract class TiffCompositeColorWriter<TPixel> : TiffBaseColorWriter<TPixel> internal abstract class TiffCompositeColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private IMemoryOwner<byte> rowBuffer; private IMemoryOwner<byte> rowBuffer;
protected TiffCompositeColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) protected TiffCompositeColorWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> 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) protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{ {
if (this.rowBuffer == null) (this.rowBuffer ??= this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height)).Clear();
{
this.rowBuffer = this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height);
}
this.rowBuffer.Clear();
Span<byte> outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)]; Span<byte> outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)];
int width = this.Image.Width; int width = this.Width;
using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width); using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width);
Span<TPixel> stripPixels = stripPixelBuffer.GetSpan(); Span<TPixel> stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height; int lastRow = y + height;
int stripPixelsRowIdx = 0; int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++) for (int row = y; row < lastRow; row++)
{ {
Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row); Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width];
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++; stripPixelsRowIdx++;
} }

12
src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffGrayL16Writer(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffGrayL16Writer(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixe
public override int BitsPerPixel => 16; public override int BitsPerPixel => 16;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

12
src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffGrayWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffGrayWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel>
public override int BitsPerPixel => 8; public override int BitsPerPixel => 8;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

7
src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs

@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
public TiffPaletteWriter( public TiffPaletteWriter(
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
Size encodingSize,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel) int bitsPerPixel)
: base(frame, memoryAllocator, configuration, entriesCollector) : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
DebugGuard.NotNull(quantizer, nameof(quantizer)); DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
}); });
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize));
this.AddColorMapTag(); this.AddColorMapTag();
} }
@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{ {
int width = this.Image.Width; int width = this.quantizedFrame.Width;
if (this.BitsPerPixel == 4) if (this.BitsPerPixel == 4)
{ {

12
src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffRgbWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffRgbWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel>
public override int BitsPerPixel => 24; public override int BitsPerPixel => 24;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

4
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -160,7 +160,7 @@ internal sealed class WebpEncoderCore
// Encode additional frames // Encode additional frames
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {
@ -235,7 +235,7 @@ internal sealed class WebpEncoderCore
// Encode additional frames // Encode additional frames
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {

8
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
@ -61,6 +63,12 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
BlendMode = this.BlendMethod, BlendMode = this.BlendMethod,
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -145,6 +145,12 @@ public class WebpMetadata : IFormatMetadata<WebpMetadata>
BackgroundColor = this.BackgroundColor BackgroundColor = this.BackgroundColor
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

10
src/ImageSharp/Image.cs

@ -72,12 +72,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <summary> /// <summary>
/// Gets any metadata associated with the image. /// Gets any metadata associated with the image.
/// </summary> /// </summary>
public ImageMetadata Metadata { get; } public ImageMetadata Metadata { get; private set; }
/// <summary> /// <summary>
/// Gets the size of the image in px units. /// Gets the size of the image in px units.
/// </summary> /// </summary>
public Size Size { get; internal set; } public Size Size { get; private set; }
/// <summary> /// <summary>
/// Gets the bounds of the image. /// Gets the bounds of the image.
@ -185,6 +185,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <param name="size">The <see cref="Size"/>.</param> /// <param name="size">The <see cref="Size"/>.</param>
protected void UpdateSize(Size size) => this.Size = size; protected void UpdateSize(Size size) => this.Size = size;
/// <summary>
/// Updates the metadata of the image after mutation.
/// </summary>
/// <param name="metadata">The <see cref="Metadata"/>.</param>
protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata;
/// <summary> /// <summary>
/// Disposes the object and frees resources for the Garbage Collector. /// Disposes the object and frees resources for the Garbage Collector.
/// </summary> /// </summary>

32
src/ImageSharp/ImageFrame.cs

@ -25,25 +25,24 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata)
{ {
this.Configuration = configuration; this.Configuration = configuration;
this.Width = width; this.Size = new(width, height);
this.Height = height;
this.Metadata = metadata; this.Metadata = metadata;
} }
/// <summary> /// <summary>
/// Gets the width. /// Gets the frame width in px units.
/// </summary> /// </summary>
public int Width { get; private set; } public int Width => this.Size.Width;
/// <summary> /// <summary>
/// Gets the height. /// Gets the frame height in px units.
/// </summary> /// </summary>
public int Height { get; private set; } public int Height => this.Size.Height;
/// <summary> /// <summary>
/// Gets the metadata of the frame. /// Gets the metadata of the frame.
/// </summary> /// </summary>
public ImageFrameMetadata Metadata { get; } public ImageFrameMetadata Metadata { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public Configuration Configuration { get; } public Configuration Configuration { get; }
@ -51,8 +50,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <summary> /// <summary>
/// Gets the size of the frame. /// Gets the size of the frame.
/// </summary> /// </summary>
/// <returns>The <see cref="Size"/></returns> public Size Size { get; private set; }
public Size Size() => new(this.Width, this.Height);
/// <summary> /// <summary>
/// Gets the bounds of the frame. /// Gets the bounds of the frame.
@ -77,12 +75,14 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>; where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>;
/// <summary> /// <summary>
/// Updates the size of the image frame. /// Updates the size of the image frame after mutation.
/// </summary> /// </summary>
/// <param name="size">The size.</param> /// <param name="size">The <see cref="Size"/>.</param>
internal void UpdateSize(Size size) protected void UpdateSize(Size size) => this.Size = size;
{
this.Width = size.Width; /// <summary>
this.Height = size.Height; /// Updates the metadata of the image frame after mutation.
} /// </summary>
/// <param name="metadata">The <see cref="Metadata"/>.</param>
protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata;
} }

2
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -414,7 +414,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{ {
ImageFrame<TPixel> result = new( ImageFrame<TPixel> result = new(
this.parent.Configuration, this.parent.Configuration,
source.Size(), source.Size,
source.Metadata.DeepClone()); source.Metadata.DeepClone());
source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup);
return result; return result;

24
src/ImageSharp/ImageFrame{TPixel}.cs

@ -322,7 +322,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <exception cref="ArgumentException">ImageFrame{TPixel}.CopyTo(): target must be of the same size!</exception> /// <exception cref="ArgumentException">ImageFrame{TPixel}.CopyTo(): target must be of the same size!</exception>
internal void CopyTo(Buffer2D<TPixel> target) internal void CopyTo(Buffer2D<TPixel> target)
{ {
if (this.Size() != target.Size()) if (this.Size != target.Size())
{ {
throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target)); throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target));
} }
@ -331,17 +331,29 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="pixelSource">The pixel source.</param> /// <param name="source">The pixel source.</param>
internal void SwapOrCopyPixelsBufferFrom(ImageFrame<TPixel> pixelSource) internal void SwapOrCopyPixelsBufferFrom(ImageFrame<TPixel> source)
{ {
Guard.NotNull(pixelSource, nameof(pixelSource)); Guard.NotNull(source, nameof(source));
Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, pixelSource.PixelBuffer); Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer);
this.UpdateSize(this.PixelBuffer.Size()); this.UpdateSize(this.PixelBuffer.Size());
} }
/// <summary>
/// Copies the metadata from the source image.
/// </summary>
/// <param name="source">The metadata source.</param>
internal void CopyMetadataFrom(ImageFrame<TPixel> source)
{
Guard.NotNull(source, nameof(source));
this.UpdateMetadata(source.Metadata);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

36
src/ImageSharp/Image{TPixel}.cs

@ -395,22 +395,42 @@ public sealed class Image<TPixel> : Image
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="pixelSource">The pixel source.</param> /// <param name="source">The pixel source.</param>
internal void SwapOrCopyPixelsBuffersFrom(Image<TPixel> pixelSource) internal void SwapOrCopyPixelsBuffersFrom(Image<TPixel> source)
{ {
Guard.NotNull(pixelSource, nameof(pixelSource)); Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed(); this.EnsureNotDisposed();
ImageFrameCollection<TPixel> sourceFrames = pixelSource.Frames; ImageFrameCollection<TPixel> sourceFrames = source.Frames;
for (int i = 0; i < this.frames.Count; i++) for (int i = 0; i < this.frames.Count; i++)
{ {
this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]); this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]);
} }
this.UpdateSize(pixelSource.Size); this.UpdateSize(source.Size);
}
/// <summary>
/// Copies the metadata from the source image.
/// </summary>
/// <param name="source">The metadata source.</param>
internal void CopyMetadataFrom(Image<TPixel> source)
{
Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed();
ImageFrameCollection<TPixel> 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<ImageFrame<TPixel>> frames) private static Size ValidateFramesAndGetSize(IEnumerable<ImageFrame<TPixel>> frames)
@ -419,9 +439,9 @@ public sealed class Image<TPixel> : Image
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames)); ImageFrame<TPixel>? 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)); throw new ArgumentException("The provided frames must be of the same size.", nameof(frames));
} }

33
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.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata; namespace SixLabors.ImageSharp.Metadata;
@ -110,16 +111,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
&& this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata))
{ {
TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata());
this.formatMetadata[key] = derivedMeta; this.SetFormatMetadata(key, derivedMeta);
return derivedMeta; return derivedMeta;
} }
TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata();
this.formatMetadata[key] = newMeta; this.SetFormatMetadata(key, newMeta);
return newMeta; return newMeta;
} }
internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value) /// <summary>
/// Sets the metadata value associated with the specified key.
/// </summary>
/// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam>
/// <typeparam name="TFormatFrameMetadata">The type of format frame metadata.</typeparam>
/// <param name="key">The key of the value to set.</param>
/// <param name="value">The value to set.</param>
public void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
where TFormatMetadata : class where TFormatMetadata : class
where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata> where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata>
=> this.formatMetadata[key] = value; => this.formatMetadata[key] = value;
@ -143,4 +151,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
/// Synchronizes the profiles with the current metadata. /// Synchronizes the profiles with the current metadata.
/// </summary> /// </summary>
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
/// <summary>
/// This method is called after a process has been applied to the image frame.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
internal void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<IImageFormat, IFormatFrameMetadata> meta in this.formatMetadata)
{
meta.Value.AfterFrameApply(source, destination);
}
}
} }

16
src/ImageSharp/Metadata/ImageMetadata.cs

@ -230,6 +230,22 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
/// </summary> /// </summary>
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image.</param>
internal void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
foreach (KeyValuePair<IImageFormat, IFormatMetadata> meta in this.formatMetadata)
{
meta.Value.AfterImageApply(destination);
}
}
internal PixelTypeInfo GetDecodedPixelTypeInfo() internal PixelTypeInfo GetDecodedPixelTypeInfo()
{ {
// None found. Check if we have a decoded format to convert from. // None found. Check if we have a decoded format to convert from.

14
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -298,6 +299,19 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); 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);
}
}
/// <summary> /// <summary>
/// Synchronizes the profiles with the specified metadata. /// Synchronizes the profiles with the specified metadata.
/// </summary> /// </summary>

2
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs

@ -1,10 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
[DebuggerDisplay("{Tag} = {IsArray?\"[..]\":ToString(),nq} ({GetType().Name,nq})")]
internal abstract class ExifValue : IExifValue, IEquatable<ExifTag> internal abstract class ExifValue : IExifValue, IEquatable<ExifTag>
{ {
protected ExifValue(ExifTag tag) => this.Tag = tag; protected ExifValue(ExifTag tag) => this.Tag = tag;

2
src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Text; using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc; namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
@ -8,6 +9,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
/// <summary> /// <summary>
/// Represents a single value of the IPTC profile. /// Represents a single value of the IPTC profile.
/// </summary> /// </summary>
[DebuggerDisplay("{Tag} = {ToString(),nq} ({GetType().Name,nq})")]
public sealed class IptcValue : IDeepCloneable<IptcValue> public sealed class IptcValue : IDeepCloneable<IptcValue>
{ {
private byte[] data = Array.Empty<byte>(); private byte[] data = Array.Empty<byte>();

60
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -12,7 +12,28 @@ namespace SixLabors.ImageSharp.Processing;
public class AffineTransformBuilder public class AffineTransformBuilder
{ {
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new(); private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix3x2>> boundsMatrixFactories = new();
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
public AffineTransformBuilder()
: this(TransformSpace.Pixel)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the affine transform.
/// </param>
public AffineTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the affine transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees /// Prepends a rotation matrix using the given rotation angle in degrees
@ -31,8 +52,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians) public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend( => this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin. /// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -68,9 +88,7 @@ public class AffineTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians) public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append( => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin. /// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -145,9 +163,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend( => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
/// <summary> /// <summary>
/// Prepends a centered skew matrix from the give angles in radians. /// Prepends a centered skew matrix from the give angles in radians.
@ -156,9 +172,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend( => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin. /// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -187,9 +201,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append( => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
/// <summary> /// <summary>
/// Appends a centered skew matrix from the give angles in radians. /// Appends a centered skew matrix from the give angles in radians.
@ -198,9 +210,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append( => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin. /// Appends a skew matrix using the given angles in degrees at the given origin.
@ -267,7 +277,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Prepend(_ => matrix, _ => matrix); return this.Prepend(_ => matrix);
} }
/// <summary> /// <summary>
@ -283,7 +293,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Append(_ => matrix, _ => matrix); return this.Append(_ => matrix);
} }
/// <summary> /// <summary>
@ -340,13 +350,13 @@ public class AffineTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets. // Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Func<Size, Matrix3x2> factory in this.boundsMatrixFactories) foreach (Func<Size, Matrix3x2> factory in this.transformMatrixFactories)
{ {
matrix *= factory(size); matrix *= factory(size);
CheckDegenerate(matrix); CheckDegenerate(matrix);
} }
return TransformUtils.GetTransformedSize(size, matrix); return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace);
} }
private static void CheckDegenerate(Matrix3x2 matrix) private static void CheckDegenerate(Matrix3x2 matrix)
@ -357,17 +367,15 @@ public class AffineTransformBuilder
} }
} }
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory) private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory)
{ {
this.transformMatrixFactories.Insert(0, transformFactory); this.transformMatrixFactories.Insert(0, transformFactory);
this.boundsMatrixFactories.Insert(0, boundsFactory);
return this; return this;
} }
private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory) private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory)
{ {
this.transformMatrixFactories.Add(transformFactory); this.transformMatrixFactories.Add(transformFactory);
this.boundsMatrixFactories.Add(boundsFactory);
return this; return this;
} }
} }

6
src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs

@ -48,7 +48,6 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
Image<TPixel> clone = this.CreateTarget(); Image<TPixel> clone = this.CreateTarget();
this.CheckFrameCount(this.Source, clone); this.CheckFrameCount(this.Source, clone);
Configuration configuration = this.Configuration;
this.BeforeImageApply(clone); this.BeforeImageApply(clone);
for (int i = 0; i < this.Source.Frames.Count; i++) for (int i = 0; i < this.Source.Frames.Count; i++)
@ -77,9 +76,10 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
{ {
clone = ((ICloningImageProcessor<TPixel>)this).CloneAndExecute(); clone = ((ICloningImageProcessor<TPixel>)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.CheckFrameCount(this.Source, clone);
this.Source.SwapOrCopyPixelsBuffersFrom(clone); this.Source.SwapOrCopyPixelsBuffersFrom(clone);
this.Source.CopyMetadataFrom(clone);
} }
finally finally
{ {
@ -157,7 +157,7 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
Size destinationSize = this.GetDestinationSize(); Size destinationSize = this.GetDestinationSize();
// We will always be creating the clone even for mutate because we may need to resize the canvas. // We will always be creating the clone even for mutate because we may need to resize the canvas.
var destinationFrames = new ImageFrame<TPixel>[source.Frames.Count]; ImageFrame<TPixel>[] destinationFrames = new ImageFrame<TPixel>[source.Frames.Count];
for (int i = 0; i < destinationFrames.Length; i++) for (int i = 0; i < destinationFrames.Length; i++)
{ {
destinationFrames[i] = new ImageFrame<TPixel>( destinationFrames[i] = new ImageFrame<TPixel>(

4
src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs

@ -96,7 +96,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
} }
// Create a 0-filled buffer to use to store the result of the component convolutions // Create a 0-filled buffer to use to store the result of the component convolutions
using Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size(), AllocationOptions.Clean); using Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size, AllocationOptions.Clean);
// Perform the 1D convolutions on all the kernel components and accumulate the results // Perform the 1D convolutions on all the kernel components and accumulate the results
this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer); this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer);
@ -134,7 +134,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
Buffer2D<Vector4> processingBuffer) Buffer2D<Vector4> processingBuffer)
{ {
// Allocate the buffer with the intermediate convolution results // Allocate the buffer with the intermediate convolution results
using Buffer2D<ComplexVector4> firstPassBuffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size()); using Buffer2D<ComplexVector4> firstPassBuffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size);
// Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width // 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. // to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation.

2
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs

@ -66,7 +66,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());

2
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs

@ -51,7 +51,7 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
MemoryAllocator allocator = this.Configuration.MemoryAllocator; MemoryAllocator allocator = this.Configuration.MemoryAllocator;
using Buffer2D<TPixel> targetPixels = allocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> targetPixels = allocator.Allocate2D<TPixel>(source.Size);
source.CopyTo(targetPixels); source.CopyTo(targetPixels);

2
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

@ -38,7 +38,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int levels = Math.Clamp(this.definition.Levels, 1, 255); int levels = Math.Clamp(this.definition.Levels, 1, 255);
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height)); int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size);
source.CopyTo(targetPixels); source.CopyTo(targetPixels);

4
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs

@ -43,7 +43,7 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns> /// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeStart(float radius, float center, int min, int max) 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);
/// <summary> /// <summary>
/// Gets the end position (inclusive) for a sampling range given /// Gets the end position (inclusive) for a sampling range given
@ -56,5 +56,5 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns> /// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeEnd(float radius, float center, int min, int max) 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);
} }

7
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

@ -28,15 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this( : this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize),
sampler, sampler,
sourceSize) sourceSize)
=> this.Degrees = degrees; => this.Degrees = degrees;
// Helper constructor // Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel))
{ {
} }

7
src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs

@ -30,8 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this( : this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel),
TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize),
sampler, sampler,
sourceSize) sourceSize)
{ {
@ -40,8 +39,8 @@ public sealed class SkewProcessor : AffineTransformProcessor
} }
// Helper constructor: // Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel))
{ {
} }

39
src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs

@ -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;
/// <summary>
/// Contains helper methods for working with transforms.
/// </summary>
internal static class TransformProcessorHelpers
{
/// <summary>
/// Updates the dimensional metadata of a transformed image
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to update</param>
public static void UpdateDimensionalMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
}

9
src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs

@ -23,10 +23,17 @@ internal abstract class TransformProcessor<TPixel> : CloningImageProcessor<TPixe
{ {
} }
/// <inheritdoc/>
protected override void AfterFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
base.AfterFrameApply(source, destination);
destination.Metadata.AfterFrameApply(source, destination);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> destination) protected override void AfterImageApply(Image<TPixel> destination)
{ {
TransformProcessorHelpers.UpdateDimensionalMetadata(destination);
base.AfterImageApply(destination); base.AfterImageApply(destination);
destination.Metadata.AfterImageApply(destination);
} }
} }

290
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -68,6 +68,11 @@ internal static class TransformUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) 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; const float epsilon = 0.0000001F;
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);
@ -78,48 +83,22 @@ internal static class TransformUtils
/// </summary> /// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param> /// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix( => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace);
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary> /// <summary>
/// Creates a centered rotation transform matrix using the given rotation in radians and the source size. /// Creates a centered rotation transform matrix using the given rotation in radians and the source size.
/// </summary> /// </summary>
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix( => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace);
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
/// <summary>
/// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary>
/// Creates a centered rotation bounds matrix using the given rotation in radians and the source size.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
/// <summary> /// <summary>
/// Creates a centered skew transform matrix from the give angles in degrees and the source size. /// Creates a centered skew transform matrix from the give angles in degrees and the source size.
@ -127,12 +106,11 @@ internal static class TransformUtils
/// <param name="degreesX">The X angle, in degrees.</param> /// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix( => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace);
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
/// <summary> /// <summary>
/// Creates a centered skew transform matrix from the give angles in radians and the source size. /// Creates a centered skew transform matrix from the give angles in radians and the source size.
@ -140,81 +118,37 @@ internal static class TransformUtils
/// <param name="radiansX">The X angle, in radians.</param> /// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix( => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace);
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
/// <summary>
/// Creates a centered skew bounds matrix from the give angles in degrees and the source size.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[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));
/// <summary>
/// Creates a centered skew bounds matrix from the give angles in radians and the source size.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[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));
/// <summary> /// <summary>
/// Gets the centered transform matrix based upon the source rectangle. /// Gets the centered transform matrix based upon the source rectangle.
/// </summary> /// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when creating the centered matrix.
/// </param>
/// <returns>The <see cref="Matrix3x2"/></returns> /// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
{ {
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace);
// We invert the matrix to handle the transformation from screen to world space. // We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct. // This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted); Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
// Centered transforms must be 0 based so we offset the bounds width and height. // The source size is provided using the coordinate space of the source image.
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F); // however the transform should always be applied in the pixel space.
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F); // To account for this we offset by the size - 1 to translate to the pixel space.
float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered; 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);
/// <summary>
/// Gets the centered bounds matrix based upon the source rectangle.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredBoundsMatrix(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);
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// Translate back to world space. // Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
@ -345,52 +279,100 @@ internal static class TransformUtils
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <returns> /// <param name="size">The source size.</param>
/// The <see cref="Rectangle"/>. /// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// </returns> /// <returns>The <see cref="Size"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) => GetTransformedSize(matrix, size, transformSpace, true);
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
return new Rectangle(0, 0, transformed.Width, transformed.Height);
}
/// <summary> /// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix. /// Returns the size relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns> /// <returns>
/// The <see cref="Rectangle"/>. /// The <see cref="Size"/>.
/// </returns> /// </returns>
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); // Check if the matrix involves only affine transformations by inspecting the relevant components.
Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); // We want to use pixel space for calculations only if the transformation is purely 2D and does not include
Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); // any perspective effects, non-standard scaling, or unusual translations that could distort the image.
Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); // 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;
} }
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Returns the size relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <returns>The <see cref="Size"/>.</returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, false);
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <param name="constrain">Whether to constrain the size to ensure that the dimensions are positive.</param>
/// <returns> /// <returns>
/// The <see cref="Size"/>. /// The <see cref="Size"/>.
/// </returns> /// </returns>
public static Size GetTransformedSize(Size size, Matrix3x2 matrix) 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!"); Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -399,9 +381,24 @@ internal static class TransformUtils
return size; 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
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))
{
// 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;
} }
/// <summary> /// <summary>
@ -409,46 +406,52 @@ internal static class TransformUtils
/// </summary> /// </summary>
/// <param name="rectangle">The source rectangle.</param> /// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="bounds">The resulting bounding rectangle.</param>
/// <returns> /// <returns>
/// The <see cref="Rectangle"/>. /// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
{ {
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 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
Vector2 br = ProjectiveTransform2D(rectangle.Right, 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;
} }
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="size">The source size.</param> /// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="bounds">The resulting bounding rectangle.</param>
/// <returns> /// <returns>
/// The <see cref="Size"/>. /// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{ {
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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -482,6 +485,11 @@ internal static class TransformUtils
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); 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))); 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));
} }
} }

61
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -12,7 +12,28 @@ namespace SixLabors.ImageSharp.Processing;
public class ProjectiveTransformBuilder public class ProjectiveTransformBuilder
{ {
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new(); private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix4x4>> boundsMatrixFactories = new();
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
public ProjectiveTransformBuilder()
: this(TransformSpace.Pixel)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the projective transform.
/// </param>
public ProjectiveTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the projective transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary> /// <summary>
/// Prepends a matrix that performs a tapering projective transform. /// Prepends a matrix that performs a tapering projective transform.
@ -22,9 +43,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param> /// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Prepend( => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
/// <summary> /// <summary>
/// Appends a matrix that performs a tapering projective transform. /// Appends a matrix that performs a tapering projective transform.
@ -34,9 +53,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param> /// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Append( => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
/// <summary> /// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees. /// Prepends a centered rotation matrix using the given rotation in degrees.
@ -52,9 +69,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationRadians(float radians) public ProjectiveTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend( => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
/// <summary> /// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -88,9 +103,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationRadians(float radians) public ProjectiveTransformBuilder AppendRotationRadians(float radians)
=> this.Append( => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
/// <summary> /// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin. /// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -174,9 +187,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend( => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
/// <summary> /// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin. /// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -214,9 +225,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append( => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
/// <summary> /// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin. /// Appends a skew matrix using the given angles in degrees at the given origin.
@ -283,7 +292,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Prepend(_ => matrix, _ => matrix); return this.Prepend(_ => matrix);
} }
/// <summary> /// <summary>
@ -299,7 +308,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Append(_ => matrix, _ => matrix); return this.Append(_ => matrix);
} }
/// <summary> /// <summary>
@ -357,13 +366,13 @@ public class ProjectiveTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets. // Translate the origin matrix to cater for source rectangle offsets.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
foreach (Func<Size, Matrix4x4> factory in this.boundsMatrixFactories) foreach (Func<Size, Matrix4x4> factory in this.transformMatrixFactories)
{ {
matrix *= factory(size); matrix *= factory(size);
CheckDegenerate(matrix); CheckDegenerate(matrix);
} }
return TransformUtils.GetTransformedSize(size, matrix); return TransformUtils.GetTransformedSize(matrix, size);
} }
private static void CheckDegenerate(Matrix4x4 matrix) private static void CheckDegenerate(Matrix4x4 matrix)
@ -374,17 +383,15 @@ public class ProjectiveTransformBuilder
} }
} }
private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> transformFactory, Func<Size, Matrix4x4> boundsFactory) private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> transformFactory)
{ {
this.transformMatrixFactories.Insert(0, transformFactory); this.transformMatrixFactories.Insert(0, transformFactory);
this.boundsMatrixFactories.Insert(0, boundsFactory);
return this; return this;
} }
private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> transformFactory, Func<Size, Matrix4x4> boundsFactory) private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> transformFactory)
{ {
this.transformMatrixFactories.Add(transformFactory); this.transformMatrixFactories.Add(transformFactory);
this.boundsMatrixFactories.Add(boundsFactory);
return this; return this;
} }
} }

26
src/ImageSharp/Processing/TransformSpace.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing;
/// <summary>
/// Represents the different spaces used in transformation operations.
/// </summary>
public enum TransformSpace
{
/// <summary>
/// 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.
/// </summary>
Coordinate,
/// <summary>
/// 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.
/// </summary>
Pixel
}

18
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -381,6 +381,20 @@ public partial class PngDecoderTests
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
} }
[Theory]
[InlineData(TestImages.Png.Bad.WrongCrcDataChunk, 1)]
[InlineData(TestImages.Png.Bad.Issue2589, 24)]
public void Identify_IgnoreCrcErrors(string imagePath, int expectedPixelSize)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }, stream);
Assert.NotNull(imageInfo);
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
}
[Theory] [Theory]
[WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)]
public void Decode_MissingDataChunk_ThrowsException<TPixel>(TestImageProvider<TPixel> provider) public void Decode_MissingDataChunk_ThrowsException<TPixel>(TestImageProvider<TPixel> provider)
@ -479,7 +493,7 @@ public partial class PngDecoderTests
public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors<TPixel>(TestImageProvider<TPixel> provider, bool compare) public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors<TPixel>(TestImageProvider<TPixel> provider, bool compare)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new PngDecoderOptions() { PngCrcChunkHandling = PngCrcChunkHandling.IgnoreData }); using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData });
image.DebugSave(provider); image.DebugSave(provider);
if (compare) if (compare)
@ -660,7 +674,7 @@ public partial class PngDecoderTests
public void Binary_PrematureEof() public void Binary_PrematureEof()
{ {
PngDecoder decoder = PngDecoder.Instance; PngDecoder decoder = PngDecoder.Instance;
PngDecoderOptions options = new() { PngCrcChunkHandling = PngCrcChunkHandling.IgnoreData }; PngDecoderOptions options = new() { GeneralOptions = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData } };
using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options); using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options);
// TODO: Try to reduce this to 1. // TODO: Try to reduce this to 1.

11
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -23,7 +23,6 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public static readonly string[] MultiframeTestImages = Multiframes; public static readonly string[] MultiframeTestImages = Multiframes;
[Theory] [Theory]
[WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
[WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)]
public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider) public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider)
@ -596,6 +595,16 @@ public class TiffDecoderTests : TiffDecoderBaseTester
Assert.Equal(1, image.Frames.Count); Assert.Equal(1, image.Frames.Count);
} }
[Theory]
[WithFile(MultiFrameMipMap, PixelTypes.Rgba32)]
public void CanDecode_MultiFrameMipMap<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
Assert.Equal(7, image.Frames.Count);
image.DebugSaveMultiFrame(provider);
}
[Theory] [Theory]
[WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)]

1
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs

@ -18,7 +18,6 @@ public class TiffEncoderMultiframeTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb);
[Theory] [Theory]
[WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeMultiframe_NotSupport<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeMultiframe_NotSupport<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb));

77
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using static SixLabors.ImageSharp.Tests.TestImages.Tiff; using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
@ -292,6 +293,82 @@ public class TiffEncoderTests : TiffEncoderBaseTester
Assert.Equal(expectedCompression, frameMetaData.Compression); Assert.Equal(expectedCompression, frameMetaData.Compression);
} }
[Theory]
[WithFile(MultiFrameMipMap, PixelTypes.Rgba32)]
public void TiffEncoder_EncodesMultiFrameMipMap<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
Assert.Equal(7, image.Frames.Count);
using MemoryStream memStream = new();
image.SaveAsTiff(memStream);
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
Assert.Equal(7, image.Frames.Count);
Size size = image.Size;
List<Size> encodedDimensions = [];
foreach (ImageFrame<TPixel> 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<TPixel> output = Image.Load<TPixel>(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. // This makes sure, that when decoding a planar tiff, the planar configuration is not carried over to the encoded image.
[Theory] [Theory]
[WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)] [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)]

18
tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs

@ -219,6 +219,24 @@ public class AffineTransformTests
Assert.Equal(100, image.Height); Assert.Equal(100, image.Height);
} }
[Theory]
[WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)]
public void Issue2753<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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] [Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void Identity<TPixel>(TestImageProvider<TPixel> provider) public void Identity<TPixel>(TestImageProvider<TPixel> provider)

10
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -128,11 +128,11 @@ public class ProjectiveTransformTests
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
#pragma warning disable SA1117 // Parameters should be on same line or separate lines #pragma warning disable SA1117 // Parameters should be on same line or separate lines
var matrix = new Matrix4x4( Matrix4x4 matrix = new(
0.260987f, -0.434909f, 0, -0.0022184f, 0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f, 0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0, 0, 0, 1, 0,
52, 165, 0, 1); 52, 165, 0, 1);
#pragma warning restore SA1117 // Parameters should be on same line or separate lines #pragma warning restore SA1117 // Parameters should be on same line or separate lines
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()

5
tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms; namespace SixLabors.ImageSharp.Tests.Processing.Transforms;
@ -97,7 +98,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendRotationDegrees(builder, degrees); this.AppendRotationDegrees(builder, degrees);
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness // 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 position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix); var expected = Vector2.Transform(position, matrix);
@ -151,7 +152,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendSkewDegrees(builder, degreesX, degreesY); 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 position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix); var expected = Vector2.Transform(position, matrix);

35
tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs

@ -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<A8>(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);
}
}
}

3
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -16,7 +15,7 @@ public class ExactImageComparer : ImageComparer
ImageFrame<TPixelA> expected, ImageFrame<TPixelA> expected,
ImageFrame<TPixelB> actual) ImageFrame<TPixelB> actual)
{ {
if (expected.Size() != actual.Size()) if (expected.Size != actual.Size)
{ {
throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!");
} }

3
tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -58,7 +57,7 @@ public class TolerantImageComparer : ImageComparer
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(int index, ImageFrame<TPixelA> expected, ImageFrame<TPixelB> actual) public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(int index, ImageFrame<TPixelA> expected, ImageFrame<TPixelB> actual)
{ {
if (expected.Size() != actual.Size()) if (expected.Size != actual.Size)
{ {
throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!");
} }

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:1b926c8335eca5530d8704739cecae0799cc651139daedb1f88ac85b0ee1bd5d oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729
size 9484 size 9747

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5
size 10103 size 10470

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:322c7e061f8565efdc19642e27353ec3073ee43d8c17fbef8c13be3bb60d11dc oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db
size 10190 size 10561

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:544e7bac188d0869f98ec075fa0e73ab831e4dafe40c1520dce194df6a53c9b8 oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0
size 12737 size 13227

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:2ccc08769974e4088702d2c95fd274af7e02095955953b424a6313d656a77735 oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3
size 19974 size 20778

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:c1179b300b35d526bab148833ab6240f1207b8ade36674b1f47cc5a2d47a084c oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a
size 10603 size 10943

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f666fe67ee4a1c7152fc6190affba95ea4cbd857d96bac0968e5f1fd89792d32 oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13486 size 13536

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5b05406a1d95f0709a7aaab7c1f57ba161b7907b76746f61788cfe527796a489 oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735
size 4131 size 4165

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:be52d36cc8f616a781c8b1416ca0bf6207b9acd580e9c06e1ee5ad434d48ab38 oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13481 size 13536

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:33c99b8f0fb5d10a273a90946767f93ab6cd2dd1942f9829d695987db30dccfa oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a
size 12488 size 12559

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:af2c0201c59065a500ae985e9b7ca164e5bcb4ce2d8d8305103398830472e07c oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2
size 14206 size 14223

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4cef17988c4a3a667dede3dd86ed61d0507a84e5b846f52459683fd04e5a396a oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272
size 17297 size 17927

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9699a81572c03c2bc47d8bbdd1d64df26f87df3d4ad59fb6f164f6e82786d91d oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc
size 18853 size 18945

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4fb1f59c5393debdff9bd4b7a6c222b7a0686e6d5ef24363e3d5c94ba9b5bc27 oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af
size 20725 size 20844

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8ffa8ca6a60de9fe26a191edc2127886c61c072c1aa2b91fe3125512fe40e1b3 oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b
size 13848 size 13857

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abf1d0f323795c0aaff0ff8b488d9866c5b2f7c64aad83701cb1f60e22668b0e oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f
size 4161 size 4154

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9b0f3c41248138bd501ae844e5b54fb9f49e5d22bab9b2ef0a0654034708b99f oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07
size 14027 size 13991

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6a3f839d64984b9fda4125c6643f4699add6f95373a2194c5726ed3740565a47 oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b
size 13725 size 13744

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2ac143bc73612cecfffbec049a7b68234e7bf7581e680c3f996a977c6d671cc1 oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca
size 14865 size 14889

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4eb9dab20d5a03c0adde05a9b741d9e1b0fb8c3d79054a8bc5788de496e5c7f8 oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f
size 12420 size 12374

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0f56ee78cc2fd698ac8ea84912648f0b49d4b4d66b439f6976447c56a44c2998 oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328
size 16909 size 17027

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:dd3b29b530e221618f65cd5e493b21fe3c27804fde7664636b7bb002f72abbb2 oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921
size 3663 size 854

4
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 version https://git-lfs.github.com/spec/v1
oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f
size 3564 size 657

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save