diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
deleted file mode 100644
index 8f8e187403..0000000000
--- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats;
-
-internal class AnimatedImageFrameMetadata
-{
- ///
- /// Gets or sets the frame color table.
- ///
- public ReadOnlyMemory? ColorTable { get; set; }
-
- ///
- /// Gets or sets the frame color table mode.
- ///
- public FrameColorTableMode ColorTableMode { get; set; }
-
- ///
- /// Gets or sets the duration of the frame.
- ///
- public TimeSpan Duration { get; set; }
-
- ///
- /// Gets or sets the frame alpha blending mode.
- ///
- public FrameBlendMode BlendMode { get; set; }
-
- ///
- /// Gets or sets the frame disposal mode.
- ///
- public FrameDisposalMode DisposalMode { get; set; }
-}
diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs
deleted file mode 100644
index ac3ca29f4f..0000000000
--- a/src/ImageSharp/Formats/AnimatedImageMetadata.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats;
-
-internal class AnimatedImageMetadata
-{
- ///
- /// Gets or sets the shared color table.
- ///
- public ReadOnlyMemory? ColorTable { get; set; }
-
- ///
- /// Gets or sets the shared color table mode.
- ///
- public FrameColorTableMode ColorTableMode { get; set; }
-
- ///
- /// Gets or sets the default background color of the canvas when animating.
- /// This color may be used to fill the unused space on the canvas around the frames,
- /// as well as the transparent pixels of the first frame.
- /// The background color is also used when the disposal mode is .
- ///
- public Color BackgroundColor { get; set; }
-
- ///
- /// Gets or sets the number of times any animation is repeated.
- ///
- /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
- ///
- ///
- public ushort RepeatCount { get; set; }
-}
diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
index 68e99bdc5f..d0c60421c4 100644
--- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
@@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata
///
public BmpMetadata DeepClone() => new(this);
+
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
}
diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
index a8a51878e0..6fc8905279 100644
--- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
+++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
@@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
- curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
- curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
- curMetadata.HotspotX = curFrameMetadata.HotspotX;
- curMetadata.HotspotY = curFrameMetadata.HotspotY;
}
}
}
diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
index 06cf426dc4..4e9a432b16 100644
--- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
@@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata
EncodingHeight = this.EncodingHeight
};
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ float ratioX = destination.Width / (float)source.Width;
+ float ratioY = destination.Height / (float)source.Height;
+ this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
+ this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata
ColorType = color
};
}
+
+ private static byte Scale(byte? value, int destination, float ratio)
+ {
+ if (value is null)
+ {
+ return (byte)Math.Clamp(destination, 0, 255);
+ }
+
+ return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ }
}
diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs
index 6e97a8584a..19de7f434d 100644
--- a/src/ImageSharp/Formats/Cur/CurMetadata.cs
+++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs
@@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata
private CurMetadata(CurMetadata other)
{
this.Compression = other.Compression;
- this.HotspotX = other.HotspotX;
- this.HotspotY = other.HotspotY;
- this.EncodingWidth = other.EncodingWidth;
- this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0)
@@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata
///
public IconFrameCompression Compression { get; set; }
- ///
- /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
- ///
- public ushort HotspotX { get; set; }
-
- ///
- /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
- ///
- public ushort HotspotY { get; set; }
-
- ///
- /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingWidth { get; set; }
-
- ///
- /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingHeight { get; set; }
-
///
/// Gets or sets the number of bits per pixel.
/// Used when is
@@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata
ColorTable = this.ColorTable
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 11185d90b0..2e05ef782f 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -209,7 +209,7 @@ internal sealed class GifEncoderCore
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
{
diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
index f81329e973..5fe892c656 100644
--- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
@@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata
};
}
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
///
public GifFrameMetadata DeepClone() => new(this);
-
- internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
- {
- // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
- int index = -1;
- const float background = 1f;
- if (metadata.ColorTable.HasValue)
- {
- ReadOnlySpan colorTable = metadata.ColorTable.Value.Span;
- for (int i = 0; i < colorTable.Length; i++)
- {
- Vector4 vector = colorTable[i].ToScaledVector4();
- if (vector.W < background)
- {
- index = i;
- }
- }
- }
-
- bool hasTransparency = index >= 0;
-
- return new()
- {
- LocalColorTable = metadata.ColorTable,
- ColorTableMode = metadata.ColorTableMode,
- FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
- DisposalMode = metadata.DisposalMode,
- HasTransparency = hasTransparency,
- TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
- };
- }
}
diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs
index 565038b55a..517609af45 100644
--- a/src/ImageSharp/Formats/Gif/GifMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs
@@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata
};
}
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/IFormatFrameMetadata.cs b/src/ImageSharp/Formats/IFormatFrameMetadata.cs
index 4eef93ad34..20f27d050c 100644
--- a/src/ImageSharp/Formats/IFormatFrameMetadata.cs
+++ b/src/ImageSharp/Formats/IFormatFrameMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats;
///
@@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable
///
/// The .
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
+
+ ///
+ /// This method is called after a process has been applied to the image frame.
+ ///
+ /// The type of pixel format.
+ /// The source image frame.
+ /// The destination image frame.
+ void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel;
}
///
diff --git a/src/ImageSharp/Formats/IFormatMetadata.cs b/src/ImageSharp/Formats/IFormatMetadata.cs
index 8d695306e4..a351431c94 100644
--- a/src/ImageSharp/Formats/IFormatMetadata.cs
+++ b/src/ImageSharp/Formats/IFormatMetadata.cs
@@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable
///
/// The .
FormatConnectingMetadata ToFormatConnectingMetadata();
+
+ ///
+ /// This method is called after a process has been applied to the image.
+ ///
+ /// The type of pixel format.
+ /// The destination image .
+ void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel;
}
///
diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
index 8b59974eb3..b8a1dded15 100644
--- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
+++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
@@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
- curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
- curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
}
}
}
diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
index c244e38981..a2d1c01391 100644
--- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
@@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata
EncodingHeight = this.EncodingHeight
};
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ float ratioX = destination.Width / (float)source.Width;
+ float ratioY = destination.Height / (float)source.Height;
+ this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
+ this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata
ColorType = color
};
}
+
+ private static byte Scale(byte? value, int destination, float ratio)
+ {
+ if (value is null)
+ {
+ return (byte)Math.Clamp(destination, 0, 255);
+ }
+
+ return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ }
}
diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
index 7e31468ecc..a6c2704b31 100644
--- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs
+++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
@@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata
private IcoMetadata(IcoMetadata other)
{
this.Compression = other.Compression;
- this.EncodingWidth = other.EncodingWidth;
- this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0)
@@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata
///
public IconFrameCompression Compression { get; set; }
- ///
- /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingWidth { get; set; }
-
- ///
- /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
- ///
- public byte EncodingHeight { get; set; }
-
///
/// Gets or sets the number of bits per pixel.
/// Used when is
@@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata
ColorTable = this.ColorTable
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
index f2f34ec496..fe4855dc77 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
@@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata
Quality = this.Quality,
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
index fec4beda7c..d852f3c8eb 100644
--- a/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
+++ b/src/ImageSharp/Formats/Pbm/PbmMetadata.cs
@@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo(),
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 4bbb68358f..978b9184e9 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -231,7 +231,7 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
index c142a1c8e0..b8086cd6d1 100644
--- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata
};
}
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index a7b3672ef5..00cba088cb 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
index e2062014d7..e463d511d2 100644
--- a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
+++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
@@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Tga/TgaMetadata.cs b/src/ImageSharp/Formats/Tga/TgaMetadata.cs
index 58b5119523..8d40f86464 100644
--- a/src/ImageSharp/Formats/Tga/TgaMetadata.cs
+++ b/src/ImageSharp/Formats/Tga/TgaMetadata.cs
@@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 2356d45e47..d699a7b631 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
+ Size? size = null;
uint frameCount = 0;
foreach (ExifProfile ifd in directories)
{
cancellationToken.ThrowIfCancellationRequested();
- ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
+ ImageFrame frame = this.DecodeFrame(ifd, size, cancellationToken);
+
+ if (!size.HasValue)
+ {
+ size = frame.Size;
+ }
+
frames.Add(frame);
framesMetadata.Add(frame.Metadata);
@@ -181,19 +188,8 @@ internal class TiffDecoderCore : ImageDecoderCore
}
}
+ this.Dimensions = frames[0].Size;
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
-
- // TODO: Tiff frames can have different sizes.
- ImageFrame root = frames[0];
- this.Dimensions = root.Size();
- foreach (ImageFrame frame in frames)
- {
- if (frame.Size() != root.Size())
- {
- TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
- }
- }
-
return new Image(this.configuration, metadata, frames);
}
catch
@@ -215,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore
IList directories = reader.Read();
List framesMetadata = [];
- foreach (ExifProfile dir in directories)
+ int width = 0;
+ int height = 0;
+
+ for (int i = 0; i < directories.Count; i++)
{
- framesMetadata.Add(this.CreateFrameMetadata(dir));
- }
+ (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta
+ = this.CreateFrameMetadata(directories[i]);
- ExifProfile rootFrameExifProfile = directories[0];
+ framesMetadata.Add(meta.FrameMetadata);
- ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
+ width = Math.Max(width, meta.TiffMetadata.EncodingWidth);
+ height = Math.Max(height, meta.TiffMetadata.EncodingHeight);
+ }
- int width = GetImageWidth(rootFrameExifProfile);
- int height = GetImageHeight(rootFrameExifProfile);
+ ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
return new ImageInfo(new(width, height), metadata, framesMetadata);
}
@@ -235,31 +235,46 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The IFD tags.
+ /// The previously determined root frame size if decoded.
/// The token to monitor cancellation.
/// The tiff frame.
- private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken)
+ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags);
- bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata());
+ (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags);
+ 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);
- int height = GetImageHeight(tags);
- ImageFrame frame = new(this.configuration, width, height, imageFrameMetaData);
+ ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
if (isTiled)
{
- this.DecodeImageWithTiles(tags, frame, cancellationToken);
+ this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
}
else
{
- this.DecodeImageWithStrips(tags, frame, cancellationToken);
+ this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
}
return frame;
}
- private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags)
+ private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
{
ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata)
@@ -267,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore
imageFrameMetaData.ExifProfile = tags;
}
- TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags);
+ TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags);
+ imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata);
- return imageFrameMetaData;
+ return (imageFrameMetaData, tiffMetadata);
}
///
@@ -278,8 +294,10 @@ internal class TiffDecoderCore : ImageDecoderCore
/// The pixel format.
/// The IFD tags.
/// The image frame to decode into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The token to monitor cancellation.
- private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken)
+ private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int rowsPerStrip;
@@ -302,6 +320,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{
this.DecodeStripsPlanar(
frame,
+ width,
+ height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@@ -311,6 +331,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{
this.DecodeStripsChunky(
frame,
+ width,
+ height,
rowsPerStrip,
stripOffsets,
stripByteCounts,
@@ -324,13 +346,13 @@ internal class TiffDecoderCore : ImageDecoderCore
/// The pixel format.
/// The IFD tags.
/// The image frame to decode into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The token to monitor cancellation.
- private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken)
+ private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Buffer2D pixels = frame.PixelBuffer;
- int width = pixels.Width;
- int height = pixels.Height;
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue valueWidth))
{
@@ -384,11 +406,20 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The image frame to decode data into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The number of rows per strip of data.
/// An array of byte offsets to each strip in the image.
/// An array of the size of each strip (in bytes).
/// The token to monitor cancellation.
- private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken)
+ private void DecodeStripsPlanar(
+ ImageFrame frame,
+ int width,
+ int height,
+ int rowsPerStrip,
+ Span stripOffsets,
+ Span stripByteCounts,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int stripsPerPixel = this.BitsPerSample.Channels;
@@ -403,18 +434,18 @@ internal class TiffDecoderCore : ImageDecoderCore
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
+ int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize);
}
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel);
TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder();
for (int i = 0; i < stripsPerPlane; i++)
{
cancellationToken.ThrowIfCancellationRequested();
- int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
+ int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip;
int stripIndex = i;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
@@ -430,7 +461,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripIndex += stripsPerPlane;
}
- colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight);
+ colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
}
}
finally
@@ -447,39 +478,48 @@ internal class TiffDecoderCore : ImageDecoderCore
///
/// The pixel format.
/// The image frame to decode data into.
+ /// The width in px units of the frame data.
+ /// The height in px units of the frame data.
/// The rows per strip.
/// The strip offsets.
/// The strip byte counts.
/// The token to monitor cancellation.
- private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken)
+ private void DecodeStripsChunky(
+ ImageFrame frame,
+ int width,
+ int height,
+ int rowsPerStrip,
+ Span stripOffsets,
+ Span stripByteCounts,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{
- rowsPerStrip = frame.Height;
+ rowsPerStrip = height;
}
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
+ int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean);
Span stripBufferSpan = stripBuffer.GetSpan();
Buffer2D pixels = frame.PixelBuffer;
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel);
TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
- int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
+ int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip
- : frame.Height % rowsPerStrip;
+ : height % rowsPerStrip;
int top = rowsPerStrip * stripIndex;
- if (top + stripHeight > frame.Height)
+ if (top + stripHeight > height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present).
break;
@@ -493,7 +533,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripBufferSpan,
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;
}
- ///
- /// Gets the width of the image frame.
- ///
- /// The image frame exif profile.
- /// The image width.
- private static int GetImageWidth(ExifProfile exifProfile)
- {
- if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue width))
- {
- TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
- }
-
- DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
-
- return (int)width.Value;
- }
-
- ///
- /// Gets the height of the image frame.
- ///
- /// The image frame exif profile.
- /// The image height.
- private static int GetImageHeight(ExifProfile exifProfile)
- {
- if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue height))
- {
- TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
- }
-
- return (int)height.Value;
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 5f91fd7393..b560067f3f 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore
long ifdOffset)
where TPixel : unmanaged, IPixel
{
+ // Get the width and height of the frame.
+ // This can differ from the frame bounds in-memory if the image represents only
+ // a subregion.
+ TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata();
+ int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width;
+ int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height;
+
+ width = Math.Min(width, frame.Width);
+ height = Math.Min(height, frame.Height);
+ Size encodingSize = new(width, height);
+
using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
compression,
writer.BaseStream,
this.memoryAllocator,
- frame.Width,
+ width,
(int)bitsPerPixel,
this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore
using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation,
frame,
+ encodingSize,
this.quantizer,
this.pixelSamplingStrategy,
this.memoryAllocator,
@@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore
entriesCollector,
(int)bitsPerPixel);
- int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
+ int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip);
@@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore
// Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
- entriesCollector.ProcessFrameInfo(frame, imageMetadata);
+ entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata);
entriesCollector.ProcessImageFormat(this);
if (writer.Position % 2 != 0)
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index c8e28111ec..803b77fb0a 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
@@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata);
- public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
- => new FrameInfoProcessor(this).Process(frame, imageMetadata);
+ public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
+ => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata);
public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(encoder);
@@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector
{
}
- public void Process(ImageFrame frame, ImageMetadata imageMetadata)
+ public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
{
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth)
{
- Value = (uint)frame.Width
+ Value = (uint)encodingSize.Width
});
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength)
{
- Value = (uint)frame.Height
+ Value = (uint)encodingSize.Height
});
this.ProcessResolution(imageMetadata);
diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
index bb5da37411..189fee8b0c 100644
--- a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
@@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff;
@@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata
this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor;
this.InkSet = other.InkSet;
+ this.EncodingWidth = other.EncodingWidth;
+ this.EncodingHeight = other.EncodingHeight;
}
///
@@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata
///
public TiffInkSet? InkSet { get; set; }
+ ///
+ /// Gets or sets the encoding width.
+ ///
+ public int EncodingWidth { get; set; }
+
+ ///
+ /// Gets or sets the encoding height.
+ ///
+ public int EncodingHeight { get; set; }
+
///
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
- => new();
+ {
+ TiffFrameMetadata frameMetadata = new();
+ if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue)
+ {
+ frameMetadata.EncodingWidth = metadata.EncodingWidth.Value;
+ frameMetadata.EncodingHeight = metadata.EncodingHeight.Value;
+ }
+
+ return frameMetadata;
+ }
///
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
- => new();
+ => new()
+ {
+ EncodingWidth = this.EncodingWidth,
+ EncodingHeight = this.EncodingHeight
+ };
+
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ float ratioX = destination.Width / (float)source.Width;
+ float ratioY = destination.Height / (float)source.Height;
+ this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
+ this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
+
+ // Overwrite the EXIF dimensional metadata with the encoding dimensions of the image.
+ destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight);
+ }
+
+ private static int Scale(int value, int destination, float ratio)
+ {
+ if (value <= 0)
+ {
+ return destination;
+ }
+
+ return Math.Min((int)MathF.Ceiling(value * ratio), destination);
+ }
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata
///
/// The tiff frame meta data.
/// The Exif profile containing tiff frame directory tags.
- internal static void Parse(TiffFrameMetadata meta, ExifProfile profile)
+ private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
{
- if (profile != null)
+ meta.EncodingWidth = GetImageWidth(profile);
+ meta.EncodingHeight = GetImageHeight(profile);
+
+ if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue)
+ && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
+ {
+ meta.BitsPerSample = bitsPerSample;
+ }
+
+ meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
+
+ if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue))
{
- if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue? bitsPerSampleValue)
- && TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
- {
- meta.BitsPerSample = bitsPerSample;
- }
-
- meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
-
- if (profile.TryGetValue(ExifTag.Compression, out IExifValue? compressionValue))
- {
- meta.Compression = (TiffCompression)compressionValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue))
- {
- meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue))
- {
- meta.Predictor = (TiffPredictor)predictorValue.Value;
- }
-
- if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue))
- {
- meta.InkSet = (TiffInkSet)inkSetValue.Value;
- }
-
- // TODO: Why do we remove this? Encoding should overwrite.
- profile.RemoveValue(ExifTag.BitsPerSample);
- profile.RemoveValue(ExifTag.Compression);
- profile.RemoveValue(ExifTag.PhotometricInterpretation);
- profile.RemoveValue(ExifTag.Predictor);
+ meta.Compression = (TiffCompression)compressionValue.Value;
}
+
+ if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue? photometricInterpretationValue))
+ {
+ meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
+ }
+
+ if (profile.TryGetValue(ExifTag.Predictor, out IExifValue? predictorValue))
+ {
+ meta.Predictor = (TiffPredictor)predictorValue.Value;
+ }
+
+ if (profile.TryGetValue(ExifTag.InkSet, out IExifValue? inkSetValue))
+ {
+ meta.InkSet = (TiffInkSet)inkSetValue.Value;
+ }
+
+ // Remove values, we've explicitly captured them and they could change on encode.
+ profile.RemoveValue(ExifTag.BitsPerSample);
+ profile.RemoveValue(ExifTag.Compression);
+ profile.RemoveValue(ExifTag.PhotometricInterpretation);
+ profile.RemoveValue(ExifTag.Predictor);
+ }
+
+ ///
+ /// Gets the width of the image frame.
+ ///
+ /// The image frame exif profile.
+ /// The image width.
+ private static int GetImageWidth(ExifProfile exifProfile)
+ {
+ if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue? width))
+ {
+ TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
+ }
+
+ DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
+
+ return (int)width.Value;
+ }
+
+ ///
+ /// Gets the height of the image frame.
+ ///
+ /// The image frame exif profile.
+ /// The image height.
+ private static int GetImageHeight(ExifProfile exifProfile)
+ {
+ if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue? height))
+ {
+ TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
+ }
+
+ return (int)height.Value;
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
index cc70941d51..e965fcb4f6 100644
--- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs
@@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata
PixelTypeInfo = this.GetPixelTypeInfo()
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
index c4a7492553..9fd730f416 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
@@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter : IDisposable
{
private bool isDisposed;
- protected TiffBaseColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
+ protected TiffBaseColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
{
+ this.Width = encodingSize.Width;
+ this.Height = encodingSize.Height;
this.Image = image;
this.MemoryAllocator = memoryAllocator;
this.Configuration = configuration;
@@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter : IDisposable
///
public abstract int BitsPerPixel { get; }
+ ///
+ /// Gets the width of the portion of the image to be encoded.
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height of the portion of the image to be encoded.
+ ///
+ public int Height { get; }
+
///
/// Gets the bytes per row.
///
- public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8);
+ public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8);
protected ImageFrame Image { get; }
@@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter : IDisposable
public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip)
{
DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer");
- int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip;
+ int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip;
uint[] stripOffsets = new uint[stripsCount];
uint[] stripByteCounts = new uint[stripsCount];
int stripIndex = 0;
compressor.Initialize(rowsPerStrip);
- for (int y = 0; y < this.Image.Height; y += rowsPerStrip)
+ for (int y = 0; y < this.Height; y += rowsPerStrip)
{
long offset = compressor.Output.Position;
- int height = Math.Min(rowsPerStrip, this.Image.Height - y);
+ int height = Math.Min(rowsPerStrip, this.Height - y);
this.EncodeStrip(y, height, compressor);
long endOffset = compressor.Output.Position;
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
index a6f4c31060..647ff8a1a3 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
@@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
private IMemoryOwner bitStrip;
- public TiffBiColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffBiColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
// Convert image to black and white.
- this.imageBlackWhite = new Image(configuration, new ImageMetadata(), new[] { image.Clone() });
+ this.imageBlackWhite = new Image(configuration, new ImageMetadata(), [image.Clone()]);
this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg));
}
@@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
///
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- int width = this.Image.Width;
+ int width = this.Width;
- if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax)
+ if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax)
{
// Special case for T4BitCompressor.
int stripPixels = width * height;
@@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter : TiffBaseColorWriter
int bitIndex = 0;
int byteIndex = 0;
Span outputRow = rows[(outputRowIdx * this.BytesPerRow)..];
- Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row);
+ Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width];
PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width);
- for (int x = 0; x < this.Image.Width; x++)
+ for (int x = 0; x < this.Width; x++)
{
int shift = 7 - bitIndex;
if (pixelAsGraySpan[x] == 255)
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
index 96c8aeb324..31a1b0e414 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
@@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory
public static TiffBaseColorWriter Create(
TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame image,
+ Size encodingSize,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
@@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
where TPixel : unmanaged, IPixel
- {
- switch (photometricInterpretation)
+ => photometricInterpretation switch
{
- case TiffPhotometricInterpretation.PaletteColor:
- return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
- case TiffPhotometricInterpretation.BlackIsZero:
- case TiffPhotometricInterpretation.WhiteIsZero:
- return bitsPerPixel switch
- {
- 1 => new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector),
- 16 => new TiffGrayL16Writer(image, memoryAllocator, configuration, entriesCollector),
- _ => new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector)
- };
-
- default:
- return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector);
- }
- }
+ TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel),
+ TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch
+ {
+ 1 => new TiffBiColorWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ 16 => new TiffGrayL16Writer(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ _ => new TiffGrayWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector)
+ },
+ _ => new TiffRgbWriter(image, encodingSize, memoryAllocator, configuration, entriesCollector),
+ };
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
index 007857148a..67dde493c5 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
@@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
///
/// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr).
///
+/// The tpe of pixel format.
internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter
where TPixel : unmanaged, IPixel
{
private IMemoryOwner rowBuffer;
- protected TiffCompositeColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ protected TiffCompositeColorWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- if (this.rowBuffer == null)
- {
- this.rowBuffer = this.MemoryAllocator.Allocate(this.BytesPerRow * height);
- }
-
- this.rowBuffer.Clear();
+ (this.rowBuffer ??= this.MemoryAllocator.Allocate(this.BytesPerRow * height)).Clear();
Span outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)];
- int width = this.Image.Width;
+ int width = this.Width;
using IMemoryOwner stripPixelBuffer = this.MemoryAllocator.Allocate(height * width);
Span stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height;
int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
- Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row);
+ Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width];
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++;
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
index 3e0e074e95..857f551f41 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffGrayL16Writer(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffGrayL16Writer(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter 16;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
index b2a476b9aa..4a037f0d33 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayWriter : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffGrayWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffGrayWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter : TiffCompositeColorWriter
public override int BitsPerPixel => 8;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
index d9a0960d9b..da66373631 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
@@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
public TiffPaletteWriter(
ImageFrame frame,
+ Size encodingSize,
IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel)
- : base(frame, memoryAllocator, configuration, entriesCollector)
+ : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector)
{
DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
@@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
});
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
- this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
+ this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize));
this.AddColorMapTag();
}
@@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter : TiffBaseColorWriter
///
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- int width = this.Image.Width;
+ int width = this.quantizedFrame.Width;
if (this.BitsPerPixel == 4)
{
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
index 3494b6ceae..93c46a92e4 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
@@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffRgbWriter : TiffCompositeColorWriter
where TPixel : unmanaged, IPixel
{
- public TiffRgbWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
- : base(image, memoryAllocator, configuration, entriesCollector)
+ public TiffRgbWriter(
+ ImageFrame image,
+ Size encodingSize,
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ TiffEncoderEntriesCollector entriesCollector)
+ : base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{
}
@@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter : TiffCompositeColorWriter
public override int BitsPerPixel => 24;
///
- protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
+ protected override void EncodePixels(Span pixels, Span buffer)
+ => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
}
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index e37462fda4..733801d636 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -160,7 +160,7 @@ internal sealed class WebpEncoderCore
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
{
@@ -235,7 +235,7 @@ internal sealed class WebpEncoderCore
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
{
diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
index 45e182d223..3865f9837f 100644
--- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats.Webp;
///
@@ -61,6 +63,12 @@ public class WebpFrameMetadata : IFormatFrameMetadata
BlendMode = this.BlendMethod,
};
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
index 33ebbbf6dc..db57bd8f27 100644
--- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
@@ -145,6 +145,12 @@ public class WebpMetadata : IFormatMetadata
BackgroundColor = this.BackgroundColor
};
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
///
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs
index d4f773abe1..07b40a41a1 100644
--- a/src/ImageSharp/Image.cs
+++ b/src/ImageSharp/Image.cs
@@ -72,12 +72,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
///
/// Gets any metadata associated with the image.
///
- public ImageMetadata Metadata { get; }
+ public ImageMetadata Metadata { get; private set; }
///
/// Gets the size of the image in px units.
///
- public Size Size { get; internal set; }
+ public Size Size { get; private set; }
///
/// Gets the bounds of the image.
@@ -185,6 +185,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// The .
protected void UpdateSize(Size size) => this.Size = size;
+ ///
+ /// Updates the metadata of the image after mutation.
+ ///
+ /// The .
+ protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata;
+
///
/// Disposes the object and frees resources for the Garbage Collector.
///
diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs
index 2558e1a13a..fdde5019e1 100644
--- a/src/ImageSharp/ImageFrame.cs
+++ b/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)
{
this.Configuration = configuration;
- this.Width = width;
- this.Height = height;
+ this.Size = new(width, height);
this.Metadata = metadata;
}
///
- /// Gets the width.
+ /// Gets the frame width in px units.
///
- public int Width { get; private set; }
+ public int Width => this.Size.Width;
///
- /// Gets the height.
+ /// Gets the frame height in px units.
///
- public int Height { get; private set; }
+ public int Height => this.Size.Height;
///
/// Gets the metadata of the frame.
///
- public ImageFrameMetadata Metadata { get; }
+ public ImageFrameMetadata Metadata { get; private set; }
///
public Configuration Configuration { get; }
@@ -51,8 +50,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
///
/// Gets the size of the frame.
///
- /// The
- public Size Size() => new(this.Width, this.Height);
+ public Size Size { get; private set; }
///
/// Gets the bounds of the frame.
@@ -77,12 +75,14 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
where TDestinationPixel : unmanaged, IPixel;
///
- /// Updates the size of the image frame.
+ /// Updates the size of the image frame after mutation.
///
- /// The size.
- internal void UpdateSize(Size size)
- {
- this.Width = size.Width;
- this.Height = size.Height;
- }
+ /// The .
+ protected void UpdateSize(Size size) => this.Size = size;
+
+ ///
+ /// Updates the metadata of the image frame after mutation.
+ ///
+ /// The .
+ protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata;
}
diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs
index e927fb0fac..ad7d719744 100644
--- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs
+++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs
@@ -414,7 +414,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer
{
ImageFrame result = new(
this.parent.Configuration,
- source.Size(),
+ source.Size,
source.Metadata.DeepClone());
source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup);
return result;
diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs
index 0b6354d05d..2287f65cd8 100644
--- a/src/ImageSharp/ImageFrame{TPixel}.cs
+++ b/src/ImageSharp/ImageFrame{TPixel}.cs
@@ -322,7 +322,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource
/// ImageFrame{TPixel}.CopyTo(): target must be of the same size!
internal void CopyTo(Buffer2D target)
{
- if (this.Size() != target.Size())
+ if (this.Size != target.Size())
{
throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target));
}
@@ -331,17 +331,29 @@ public sealed class ImageFrame : ImageFrame, IPixelSource
}
///
- /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer.
+ /// Switches the buffers used by the image and the pixel source meaning that the Image will "own" the buffer
+ /// from the pixelSource and the pixel source will now own the Image buffer.
///
- /// The pixel source.
- internal void SwapOrCopyPixelsBufferFrom(ImageFrame pixelSource)
+ /// The pixel source.
+ internal void SwapOrCopyPixelsBufferFrom(ImageFrame source)
{
- Guard.NotNull(pixelSource, nameof(pixelSource));
+ Guard.NotNull(source, nameof(source));
- Buffer2D.SwapOrCopyContent(this.PixelBuffer, pixelSource.PixelBuffer);
+ Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer);
this.UpdateSize(this.PixelBuffer.Size());
}
+ ///
+ /// Copies the metadata from the source image.
+ ///
+ /// The metadata source.
+ internal void CopyMetadataFrom(ImageFrame source)
+ {
+ Guard.NotNull(source, nameof(source));
+
+ this.UpdateMetadata(source.Metadata);
+ }
+
///
protected override void Dispose(bool disposing)
{
diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs
index e12631cbd7..02403923d2 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -395,22 +395,42 @@ public sealed class Image : Image
}
///
- /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer.
+ /// Switches the buffers used by the image and the pixel source meaning that the Image will
+ /// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer.
///
- /// The pixel source.
- internal void SwapOrCopyPixelsBuffersFrom(Image pixelSource)
+ /// The pixel source.
+ internal void SwapOrCopyPixelsBuffersFrom(Image source)
{
- Guard.NotNull(pixelSource, nameof(pixelSource));
+ Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed();
- ImageFrameCollection sourceFrames = pixelSource.Frames;
+ ImageFrameCollection sourceFrames = source.Frames;
for (int i = 0; i < this.frames.Count; i++)
{
this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]);
}
- this.UpdateSize(pixelSource.Size);
+ this.UpdateSize(source.Size);
+ }
+
+ ///
+ /// Copies the metadata from the source image.
+ ///
+ /// The metadata source.
+ internal void CopyMetadataFrom(Image source)
+ {
+ Guard.NotNull(source, nameof(source));
+
+ this.EnsureNotDisposed();
+
+ ImageFrameCollection sourceFrames = source.Frames;
+ for (int i = 0; i < this.frames.Count; i++)
+ {
+ this.frames[i].CopyMetadataFrom(sourceFrames[i]);
+ }
+
+ this.UpdateMetadata(source.Metadata);
}
private static Size ValidateFramesAndGetSize(IEnumerable> frames)
@@ -419,9 +439,9 @@ public sealed class Image : Image
ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
- Size rootSize = rootFrame.Size();
+ Size rootSize = rootFrame.Size;
- if (frames.Any(f => f.Size() != rootSize))
+ if (frames.Any(f => f.Size != rootSize))
{
throw new ArgumentException("The provided frames must be of the same size.", nameof(frames));
}
diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
index 9c0de1edbe..b24aa140fc 100644
--- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
@@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata;
@@ -110,16 +111,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable
&& this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata))
{
TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata());
- this.formatMetadata[key] = derivedMeta;
+ this.SetFormatMetadata(key, derivedMeta);
return derivedMeta;
}
TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata();
- this.formatMetadata[key] = newMeta;
+ this.SetFormatMetadata(key, newMeta);
return newMeta;
}
- internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value)
+ ///
+ /// Sets the metadata value associated with the specified key.
+ ///
+ /// The type of format metadata.
+ /// The type of format frame metadata.
+ /// The key of the value to set.
+ /// The value to set.
+ public void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value)
where TFormatMetadata : class
where TFormatFrameMetadata : class, IFormatFrameMetadata
=> this.formatMetadata[key] = value;
@@ -143,4 +151,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable
/// Synchronizes the profiles with the current metadata.
///
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
+
+ ///
+ /// This method is called after a process has been applied to the image frame.
+ ///
+ /// The type of pixel format.
+ /// The source image frame.
+ /// The destination image frame.
+ internal void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ // Always updated using the full frame dimensions.
+ // Individual format frame metadata will update with sub region dimensions if appropriate.
+ this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
+
+ foreach (KeyValuePair meta in this.formatMetadata)
+ {
+ meta.Value.AfterFrameApply(source, destination);
+ }
+ }
}
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index 37557ba1dc..1961dbf192 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -230,6 +230,22 @@ public sealed class ImageMetadata : IDeepCloneable
///
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
+ ///
+ /// This method is called after a process has been applied to the image.
+ ///
+ /// The type of pixel format.
+ /// The destination image.
+ internal void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
+
+ foreach (KeyValuePair meta in this.formatMetadata)
+ {
+ meta.Value.AfterImageApply(destination);
+ }
+ }
+
internal PixelTypeInfo GetDecodedPixelTypeInfo()
{
// None found. Check if we have a decoded format to convert from.
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
index 41d3c293b6..e91a69444d 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.PixelFormats;
+using static System.Runtime.InteropServices.JavaScript.JSType;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -298,6 +299,19 @@ public sealed class ExifProfile : IDeepCloneable
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution);
}
+ internal void SyncDimensions(int width, int height)
+ {
+ if (this.TryGetValue(ExifTag.PixelXDimension, out _))
+ {
+ this.SetValue(ExifTag.PixelXDimension, width);
+ }
+
+ if (this.TryGetValue(ExifTag.PixelYDimension, out _))
+ {
+ this.SetValue(ExifTag.PixelYDimension, height);
+ }
+ }
+
///
/// Synchronizes the profiles with the specified metadata.
///
diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs
index 59264698bd..4ac9546f39 100644
--- a/src/ImageSharp/Processing/AffineTransformBuilder.cs
+++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs
@@ -12,7 +12,28 @@ namespace SixLabors.ImageSharp.Processing;
public class AffineTransformBuilder
{
private readonly List> transformMatrixFactories = new();
- private readonly List> boundsMatrixFactories = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AffineTransformBuilder()
+ : this(TransformSpace.Pixel)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The to use when applying the affine transform.
+ ///
+ public AffineTransformBuilder(TransformSpace transformSpace)
+ => this.TransformSpace = transformSpace;
+
+ ///
+ /// Gets the to use when applying the affine transform.
+ ///
+ public TransformSpace TransformSpace { get; }
///
/// Prepends a rotation matrix using the given rotation angle in degrees
@@ -31,8 +52,7 @@ public class AffineTransformBuilder
/// The .
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
- size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
- size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
+ size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
///
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@@ -68,9 +88,7 @@ public class AffineTransformBuilder
/// The amount of rotation, in radians.
/// The .
public AffineTransformBuilder AppendRotationRadians(float radians)
- => this.Append(
- size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
- size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
+ => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
///
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
@@ -145,9 +163,7 @@ public class AffineTransformBuilder
/// The Y angle, in degrees.
/// The .
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
- => this.Prepend(
- size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
- size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
+ => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
///
/// Prepends a centered skew matrix from the give angles in radians.
@@ -156,9 +172,7 @@ public class AffineTransformBuilder
/// The Y angle, in radians.
/// The .
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
- => this.Prepend(
- size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
- size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
+ => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
///
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@@ -187,9 +201,7 @@ public class AffineTransformBuilder
/// The Y angle, in degrees.
/// The .
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
- => this.Append(
- size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
- size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
+ => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
///
/// Appends a centered skew matrix from the give angles in radians.
@@ -198,9 +210,7 @@ public class AffineTransformBuilder
/// The Y angle, in radians.
/// The .
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
- => this.Append(
- size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
- size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
+ => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
///
/// Appends a skew matrix using the given angles in degrees at the given origin.
@@ -267,7 +277,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{
CheckDegenerate(matrix);
- return this.Prepend(_ => matrix, _ => matrix);
+ return this.Prepend(_ => matrix);
}
///
@@ -283,7 +293,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{
CheckDegenerate(matrix);
- return this.Append(_ => matrix, _ => matrix);
+ return this.Append(_ => matrix);
}
///
@@ -340,13 +350,13 @@ public class AffineTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
- foreach (Func factory in this.boundsMatrixFactories)
+ foreach (Func factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
- return TransformUtils.GetTransformedSize(size, matrix);
+ return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace);
}
private static void CheckDegenerate(Matrix3x2 matrix)
@@ -357,17 +367,15 @@ public class AffineTransformBuilder
}
}
- private AffineTransformBuilder Prepend(Func transformFactory, Func boundsFactory)
+ private AffineTransformBuilder Prepend(Func transformFactory)
{
this.transformMatrixFactories.Insert(0, transformFactory);
- this.boundsMatrixFactories.Insert(0, boundsFactory);
return this;
}
- private AffineTransformBuilder Append(Func transformFactory, Func boundsFactory)
+ private AffineTransformBuilder Append(Func transformFactory)
{
this.transformMatrixFactories.Add(transformFactory);
- this.boundsMatrixFactories.Add(boundsFactory);
return this;
}
}
diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
index aa000a10e7..abe32e3882 100644
--- a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
@@ -48,7 +48,6 @@ public abstract class CloningImageProcessor : ICloningImageProcessor clone = this.CreateTarget();
this.CheckFrameCount(this.Source, clone);
- Configuration configuration = this.Configuration;
this.BeforeImageApply(clone);
for (int i = 0; i < this.Source.Frames.Count; i++)
@@ -77,9 +76,10 @@ public abstract class CloningImageProcessor : ICloningImageProcessor)this).CloneAndExecute();
- // We now need to move the pixel data/size data from the clone to the source.
+ // We now need to move the pixel data/size data and any metadata from the clone to the source.
this.CheckFrameCount(this.Source, clone);
this.Source.SwapOrCopyPixelsBuffersFrom(clone);
+ this.Source.CopyMetadataFrom(clone);
}
finally
{
@@ -157,7 +157,7 @@ public abstract class CloningImageProcessor : ICloningImageProcessor[source.Frames.Count];
+ ImageFrame[] destinationFrames = new ImageFrame[source.Frames.Count];
for (int i = 0; i < destinationFrames.Length; i++)
{
destinationFrames[i] = new ImageFrame(
diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
index e4b0a60ab0..5931b7c402 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
@@ -96,7 +96,7 @@ internal class BokehBlurProcessor : ImageProcessor
}
// Create a 0-filled buffer to use to store the result of the component convolutions
- using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size(), AllocationOptions.Clean);
+ using Buffer2D processingBuffer = this.Configuration.MemoryAllocator.Allocate2D(source.Size, AllocationOptions.Clean);
// Perform the 1D convolutions on all the kernel components and accumulate the results
this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer);
@@ -134,7 +134,7 @@ internal class BokehBlurProcessor : ImageProcessor
Buffer2D processingBuffer)
{
// Allocate the buffer with the intermediate convolution results
- using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size());
+ using Buffer2D firstPassBuffer = configuration.MemoryAllocator.Allocate2D(source.Size);
// Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width
// to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation.
diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
index cc6e1e5fb2..10780a21e2 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
@@ -66,7 +66,7 @@ internal class Convolution2PassProcessor : ImageProcessor
///
protected override void OnFrameApply(ImageFrame source)
{
- using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size());
+ using Buffer2D firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
index d059ebe030..ae79f2c31d 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
@@ -51,7 +51,7 @@ internal class ConvolutionProcessor : ImageProcessor
protected override void OnFrameApply(ImageFrame source)
{
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
- using Buffer2D targetPixels = allocator.Allocate2D(source.Size());
+ using Buffer2D targetPixels = allocator.Allocate2D(source.Size);
source.CopyTo(targetPixels);
diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
index 1491fe073b..f811bae0f7 100644
--- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
@@ -38,7 +38,7 @@ internal class OilPaintingProcessor : ImageProcessor
int levels = Math.Clamp(this.definition.Levels, 1, 255);
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
- using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size());
+ using Buffer2D targetPixels = this.Configuration.MemoryAllocator.Allocate2D(source.Size);
source.CopyTo(targetPixels);
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
index b5eb202c18..1f68e32744 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
@@ -43,7 +43,7 @@ internal static class LinearTransformUtility
/// The .
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeStart(float radius, float center, int min, int max)
- => Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max);
+ => Numerics.Clamp((int)MathF.Floor(center - radius), min, max);
///
/// Gets the end position (inclusive) for a sampling range given
@@ -56,5 +56,5 @@ internal static class LinearTransformUtility
/// The .
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeEnd(float radius, float center, int min, int max)
- => Numerics.Clamp((int)MathF.Floor(center + radius), min, max);
+ => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max);
}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
index 6580636a24..0af2b268a1 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
@@ -28,15 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor
/// The source image size
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
- TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
- TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize),
+ TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
- private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize)
- : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix))
+ private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
+ : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel))
{
}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
index 97b18de6c8..0bbc8e0f60 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
@@ -30,8 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
/// The source image size
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
- TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
- TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize),
+ TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel),
sampler,
sourceSize)
{
@@ -40,8 +39,8 @@ public sealed class SkewProcessor : AffineTransformProcessor
}
// Helper constructor:
- private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize)
- : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix))
+ private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
+ : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel))
{
}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs
deleted file mode 100644
index 0bb4920f0f..0000000000
--- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Metadata.Profiles.Exif;
-using SixLabors.ImageSharp.PixelFormats;
-
-namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
-
-///
-/// Contains helper methods for working with transforms.
-///
-internal static class TransformProcessorHelpers
-{
- ///
- /// Updates the dimensional metadata of a transformed image
- ///
- /// The pixel format.
- /// The image to update
- public static void UpdateDimensionalMetadata(Image image)
- where TPixel : unmanaged, IPixel
- {
- ExifProfile? profile = image.Metadata.ExifProfile;
- if (profile is null)
- {
- return;
- }
-
- // Only set the value if it already exists.
- if (profile.TryGetValue(ExifTag.PixelXDimension, out _))
- {
- profile.SetValue(ExifTag.PixelXDimension, image.Width);
- }
-
- if (profile.TryGetValue(ExifTag.PixelYDimension, out _))
- {
- profile.SetValue(ExifTag.PixelYDimension, image.Height);
- }
- }
-}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
similarity index 80%
rename from src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs
rename to src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
index 0c2c29391b..bdfac00366 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
@@ -23,10 +23,17 @@ internal abstract class TransformProcessor : CloningImageProcessor
+ protected override void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ {
+ base.AfterFrameApply(source, destination);
+ destination.Metadata.AfterFrameApply(source, destination);
+ }
+
///
protected override void AfterImageApply(Image destination)
{
- TransformProcessorHelpers.UpdateDimensionalMetadata(destination);
base.AfterImageApply(destination);
+ destination.Metadata.AfterImageApply(destination);
}
}
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
index 70112ab5a8..62ea5e830d 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
@@ -68,6 +68,11 @@ internal static class TransformUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
{
+ // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases,
+ // such as when the point is transformed behind the camera in a perspective projection.
+ // However, in many 2D contexts, negative w values are not meaningful and could cause issues
+ // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure
+ // we don't divide by a very small or negative number, effectively treating any negative w as epsilon.
const float epsilon = 0.0000001F;
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);
@@ -78,48 +83,22 @@ internal static class TransformUtils
///
/// The amount of rotation, in degrees.
/// The source image size.
+ /// The to use when creating the centered matrix.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size)
- => CreateCenteredTransformMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
+ public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace)
+ => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace);
///
/// Creates a centered rotation transform matrix using the given rotation in radians and the source size.
///
/// The amount of rotation, in radians.
/// The source image size.
+ /// The to use when creating the centered matrix.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size)
- => CreateCenteredTransformMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
-
- ///
- /// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size.
- ///
- /// The amount of rotation, in degrees.
- /// The source image size.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size)
- => CreateCenteredBoundsMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
-
- ///
- /// Creates a centered rotation bounds matrix using the given rotation in radians and the source size.
- ///
- /// The amount of rotation, in radians.
- /// The source image size.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size)
- => CreateCenteredBoundsMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
+ public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace)
+ => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace);
///
/// Creates a centered skew transform matrix from the give angles in degrees and the source size.
@@ -127,12 +106,11 @@ internal static class TransformUtils
/// The X angle, in degrees.
/// The Y angle, in degrees.
/// The source image size.
+ /// The to use when creating the centered matrix.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size)
- => CreateCenteredTransformMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
+ public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace)
+ => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace);
///
/// Creates a centered skew transform matrix from the give angles in radians and the source size.
@@ -140,81 +118,37 @@ internal static class TransformUtils
/// The X angle, in radians.
/// The Y angle, in radians.
/// The source image size.
+ /// The to use when creating the centered matrix.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size)
- => CreateCenteredTransformMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
-
- ///
- /// Creates a centered skew bounds matrix from the give angles in degrees and the source size.
- ///
- /// The X angle, in degrees.
- /// The Y angle, in degrees.
- /// The source image size.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateSkewBoundsMatrixDegrees(float degreesX, float degreesY, Size size)
- => CreateCenteredBoundsMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
-
- ///
- /// Creates a centered skew bounds matrix from the give angles in radians and the source size.
- ///
- /// The X angle, in radians.
- /// The Y angle, in radians.
- /// The source image size.
- /// The .
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Matrix3x2 CreateSkewBoundsMatrixRadians(float radiansX, float radiansY, Size size)
- => CreateCenteredBoundsMatrix(
- new Rectangle(Point.Empty, size),
- Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
+ public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace)
+ => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace);
///
/// Gets the centered transform matrix based upon the source rectangle.
///
- /// The source image bounds.
/// The transformation matrix.
+ /// The source image size.
+ ///
+ /// The to use when creating the centered matrix.
+ ///
/// The
[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.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
- // Centered transforms must be 0 based so we offset the bounds width and height.
- Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F);
- Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F);
-
- // Translate back to world space.
- Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
+ // The source size is provided using the coordinate space of the source image.
+ // however the transform should always be applied in the pixel space.
+ // To account for this we offset by the size - 1 to translate to the pixel space.
+ float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;
- return centered;
- }
-
- ///
- /// Gets the centered bounds matrix based upon the source rectangle.
- ///
- /// The source image bounds.
- /// The transformation matrix.
- /// The
- [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);
+ Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F);
+ Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
@@ -345,52 +279,100 @@ internal static class TransformUtils
}
///
- /// Returns the rectangle bounds relative to the source for the given transformation matrix.
+ /// Returns the size relative to the source for the given transformation matrix.
///
- /// The source rectangle.
/// The transformation matrix.
- ///
- /// The .
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
- {
- Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
- return new Rectangle(0, 0, transformed.Width, transformed.Height);
- }
+ /// The source size.
+ /// The to use when calculating the size.
+ /// The .
+ public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
+ => GetTransformedSize(matrix, size, transformSpace, true);
///
- /// Returns the rectangle relative to the source for the given transformation matrix.
+ /// Returns the size relative to the source for the given transformation matrix.
///
- /// The source rectangle.
/// The transformation matrix.
+ /// The source size.
///
- /// The .
+ /// The .
///
- public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size GetTransformedSize(Matrix4x4 matrix, Size size)
{
- if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
+ Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
+
+ if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{
- return rectangle;
+ return size;
}
- Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
- Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
- Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
- Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
+ // Check if the matrix involves only affine transformations by inspecting the relevant components.
+ // We want to use pixel space for calculations only if the transformation is purely 2D and does not include
+ // any perspective effects, non-standard scaling, or unusual translations that could distort the image.
+ // The conditions are as follows:
+ bool usePixelSpace =
+
+ // 1. Ensure there's no perspective distortion:
+ // M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0.
+ (matrix.M34 == 0) &&
+
+ // 2. Ensure standard affine transformation without any unusual depth or perspective scaling:
+ // M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth
+ // scaling or perspective, which suggests a more complex transformation.
+ (matrix.M44 == 1) &&
+
+ // 3. Ensure no unusual translation in the x-direction:
+ // M14 represents translation in the x-direction that might be part of a more complex transformation.
+ // For standard affine transformations, M14 should be 0.
+ (matrix.M14 == 0) &&
- return GetBoundingRectangle(tl, tr, bl, br);
+ // 4. Ensure no unusual translation in the y-direction:
+ // M24 represents translation in the y-direction that might be part of a more complex transformation.
+ // For standard affine transformations, M24 should be 0.
+ (matrix.M24 == 0);
+
+ // Define an offset size to translate between pixel space and coordinate space.
+ // When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
+ // When not using pixel space, use SizeF.Empty as the offset.
+
+ // Compute scaling factors from the matrix
+ float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
+ float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
+
+ // Apply the offset relative to the scale
+ SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty;
+
+ // Subtract the offset size to translate to the appropriate space (pixel or coordinate).
+ if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
+ {
+ // Add the offset size back to translate the transformed bounds to the correct space.
+ return Size.Ceiling(ConstrainSize(bounds) + offsetSize);
+ }
+
+ return size;
}
///
/// Returns the size relative to the source for the given transformation matrix.
///
+ /// The transformation matrix.
/// The source size.
+ /// The to use when calculating the size.
+ /// The .
+ private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
+ => GetTransformedSize(matrix, size, transformSpace, false);
+
+ ///
+ /// Returns the size relative to the source for the given transformation matrix.
+ ///
/// The transformation matrix.
+ /// The source size.
+ /// The to use when calculating the size.
+ /// Whether to constrain the size to ensure that the dimensions are positive.
///
/// The .
///
- 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!");
@@ -399,9 +381,24 @@ internal static class TransformUtils
return size;
}
- Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
+ // Define an offset size to translate between coordinate space and pixel space.
+ // Compute scaling factors from the matrix
+ 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;
}
///
@@ -409,46 +406,52 @@ internal static class TransformUtils
///
/// The source rectangle.
/// The transformation matrix.
+ /// The resulting bounding rectangle.
///
- /// The .
+ /// if the transformation was successful; otherwise, .
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
+ private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
{
- if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
+ if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
{
- return rectangle;
+ bounds = default;
+ return false;
}
- Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix);
- Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix);
- Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix);
- Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix);
+ Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
+ Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
+ Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
+ Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
- return GetBoundingRectangle(tl, tr, bl, br);
+ bounds = GetBoundingRectangle(tl, tr, bl, br);
+ return true;
}
///
- /// Returns the size relative to the source for the given transformation matrix.
+ /// Returns the rectangle relative to the source for the given transformation matrix.
///
- /// The source size.
+ /// The source rectangle.
/// The transformation matrix.
+ /// The resulting bounding rectangle.
///
- /// The .
+ /// if the transformation was successful; otherwise, .
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Size GetTransformedSize(Size size, Matrix4x4 matrix)
+ private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
{
- Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
-
- if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
+ if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
{
- return size;
+ bounds = default;
+ return false;
}
- Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
+ Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix);
+ Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix);
+ Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix);
+ Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix);
- return ConstrainSize(rectangle);
+ bounds = GetBoundingRectangle(tl, tr, bl, br);
+ return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -482,6 +485,11 @@ internal static class TransformUtils
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
- return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom));
+ // Clamp the values to the nearest whole pixel.
+ return Rectangle.FromLTRB(
+ (int)Math.Floor(left),
+ (int)Math.Floor(top),
+ (int)Math.Ceiling(right),
+ (int)Math.Ceiling(bottom));
}
}
diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
index 0387adebb9..9027ee7266 100644
--- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
+++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
@@ -12,7 +12,28 @@ namespace SixLabors.ImageSharp.Processing;
public class ProjectiveTransformBuilder
{
private readonly List> transformMatrixFactories = new();
- private readonly List> boundsMatrixFactories = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ProjectiveTransformBuilder()
+ : this(TransformSpace.Pixel)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The to use when applying the projective transform.
+ ///
+ public ProjectiveTransformBuilder(TransformSpace transformSpace)
+ => this.TransformSpace = transformSpace;
+
+ ///
+ /// Gets the to use when applying the projective transform.
+ ///
+ public TransformSpace TransformSpace { get; }
///
/// Prepends a matrix that performs a tapering projective transform.
@@ -22,9 +43,7 @@ public class ProjectiveTransformBuilder
/// The amount to taper.
/// The .
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
- => this.Prepend(
- size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
- size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
+ => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
///
/// Appends a matrix that performs a tapering projective transform.
@@ -34,9 +53,7 @@ public class ProjectiveTransformBuilder
/// The amount to taper.
/// The .
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
- => this.Append(
- size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
- size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
+ => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
///
/// Prepends a centered rotation matrix using the given rotation in degrees.
@@ -52,9 +69,7 @@ public class ProjectiveTransformBuilder
/// The amount of rotation, in radians.
/// The .
public ProjectiveTransformBuilder PrependRotationRadians(float radians)
- => this.Prepend(
- size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
- size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
+ => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
///
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@@ -88,9 +103,7 @@ public class ProjectiveTransformBuilder
/// The amount of rotation, in radians.
/// The .
public ProjectiveTransformBuilder AppendRotationRadians(float radians)
- => this.Append(
- size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
- size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
+ => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
///
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@@ -174,9 +187,7 @@ public class ProjectiveTransformBuilder
/// The Y angle, in radians.
/// The .
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
- => this.Prepend(
- size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
- size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
+ => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
///
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@@ -214,9 +225,7 @@ public class ProjectiveTransformBuilder
/// The Y angle, in radians.
/// The .
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
- => this.Append(
- size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
- size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
+ => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
///
/// Appends a skew matrix using the given angles in degrees at the given origin.
@@ -283,7 +292,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix)
{
CheckDegenerate(matrix);
- return this.Prepend(_ => matrix, _ => matrix);
+ return this.Prepend(_ => matrix);
}
///
@@ -299,7 +308,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix)
{
CheckDegenerate(matrix);
- return this.Append(_ => matrix, _ => matrix);
+ return this.Append(_ => matrix);
}
///
@@ -357,13 +366,13 @@ public class ProjectiveTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
- foreach (Func factory in this.boundsMatrixFactories)
+ foreach (Func factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
- return TransformUtils.GetTransformedSize(size, matrix);
+ return TransformUtils.GetTransformedSize(matrix, size);
}
private static void CheckDegenerate(Matrix4x4 matrix)
@@ -374,17 +383,15 @@ public class ProjectiveTransformBuilder
}
}
- private ProjectiveTransformBuilder Prepend(Func transformFactory, Func boundsFactory)
+ private ProjectiveTransformBuilder Prepend(Func transformFactory)
{
this.transformMatrixFactories.Insert(0, transformFactory);
- this.boundsMatrixFactories.Insert(0, boundsFactory);
return this;
}
- private ProjectiveTransformBuilder Append(Func transformFactory, Func boundsFactory)
+ private ProjectiveTransformBuilder Append(Func transformFactory)
{
this.transformMatrixFactories.Add(transformFactory);
- this.boundsMatrixFactories.Add(boundsFactory);
return this;
}
}
diff --git a/src/ImageSharp/Processing/TransformSpace.cs b/src/ImageSharp/Processing/TransformSpace.cs
new file mode 100644
index 0000000000..bca676bd88
--- /dev/null
+++ b/src/ImageSharp/Processing/TransformSpace.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Processing;
+
+///
+/// Represents the different spaces used in transformation operations.
+///
+public enum TransformSpace
+{
+ ///
+ /// Coordinate space is a continuous, mathematical grid where objects and positions
+ /// are defined with precise, often fractional values. This space allows for fine-grained
+ /// transformations like scaling, rotation, and translation with high precision.
+ /// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries.
+ ///
+ Coordinate,
+
+ ///
+ /// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen.
+ /// In this space, positions are defined by whole numbers, with no fractional values.
+ /// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3).
+ /// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen.
+ ///
+ Pixel
+}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 97f02f3684..8b4aa3d706 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -23,7 +23,6 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public static readonly string[] MultiframeTestImages = Multiframes;
[Theory]
- [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
[WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)]
public void ThrowsNotSupported(TestImageProvider