Browse Source

Merge pull request #471 from SixLabors/js/remove-drawing-clone

Remove Clone from DrawImage to reduce memory consumption
pull/479/head
James Jackson-South 8 years ago
committed by GitHub
parent
commit
823c0767cb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      src/ImageSharp.Drawing/DrawImage.cs
  2. 88
      src/ImageSharp.Drawing/Processors/DrawImageProcessor.cs
  3. 2
      src/ImageSharp/GraphicsOptions.cs
  4. 5
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  5. 13
      src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs
  6. 11
      src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs
  7. 39
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  8. 10
      src/ImageSharp/Processing/Transforms/Transform.cs
  9. 48
      src/ImageSharp/Processing/Transforms/TransformHelpers.cs
  10. 38
      tests/ImageSharp.Tests/Drawing/DrawImageTest.cs
  11. 2
      tests/ImageSharp.Tests/Image/PixelAccessorTests.cs

53
src/ImageSharp.Drawing/DrawImage.cs

@ -18,24 +18,13 @@ namespace SixLabors.ImageSharp
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="size">The size to draw the blended image.</param>
/// <param name="location">The location to draw the blended image.</param> /// <param name="location">The location to draw the blended image.</param>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, Size size, Point location, GraphicsOptions options) public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, Point location, GraphicsOptions options)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
if (size == default(Size)) source.ApplyProcessor(new DrawImageProcessor<TPixel>(image, location, options));
{
size = new Size(image.Width, image.Height);
}
if (location == default(Point))
{
location = Point.Empty;
}
source.ApplyProcessor(new DrawImageProcessor<TPixel>(image, size, location, options));
return source; return source;
} }
@ -45,14 +34,14 @@ namespace SixLabors.ImageSharp
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 1.</param> /// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, float percent) public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, float opacity)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
GraphicsOptions options = GraphicsOptions.Default; GraphicsOptions options = GraphicsOptions.Default;
options.BlendPercentage = percent; options.BlendPercentage = opacity;
return DrawImage(source, image, default(Size), default(Point), options); return DrawImage(source, image, Point.Empty, options);
} }
/// <summary> /// <summary>
@ -62,15 +51,15 @@ namespace SixLabors.ImageSharp
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="blender">The blending mode.</param> /// <param name="blender">The blending mode.</param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 1.</param> /// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, PixelBlenderMode blender, float percent) public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, PixelBlenderMode blender, float opacity)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
GraphicsOptions options = GraphicsOptions.Default; GraphicsOptions options = GraphicsOptions.Default;
options.BlendPercentage = percent; options.BlendPercentage = opacity;
options.BlenderMode = blender; options.BlenderMode = blender;
return DrawImage(source, image, default(Size), default(Point), options); return DrawImage(source, image, Point.Empty, options);
} }
/// <summary> /// <summary>
@ -79,12 +68,12 @@ namespace SixLabors.ImageSharp
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="options">The options, including the blending type and belnding amount.</param> /// <param name="options">The options, including the blending type and blending amount.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, GraphicsOptions options) public static IImageProcessingContext<TPixel> Blend<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, GraphicsOptions options)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
return DrawImage(source, image, default(Size), default(Point), options); return DrawImage(source, image, Point.Empty, options);
} }
/// <summary> /// <summary>
@ -93,16 +82,15 @@ namespace SixLabors.ImageSharp
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 1.</param> /// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
/// <param name="size">The size to draw the blended image.</param>
/// <param name="location">The location to draw the blended image.</param> /// <param name="location">The location to draw the blended image.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, float percent, Size size, Point location) public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, float opacity, Point location)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
GraphicsOptions options = GraphicsOptions.Default; GraphicsOptions options = GraphicsOptions.Default;
options.BlendPercentage = percent; options.BlendPercentage = opacity;
return source.DrawImage(image, size, location, options); return source.DrawImage(image, location, options);
} }
/// <summary> /// <summary>
@ -112,17 +100,16 @@ namespace SixLabors.ImageSharp
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="blender">The type of bending to apply.</param> /// <param name="blender">The type of bending to apply.</param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 1.</param> /// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
/// <param name="size">The size to draw the blended image.</param>
/// <param name="location">The location to draw the blended image.</param> /// <param name="location">The location to draw the blended image.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns> /// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, PixelBlenderMode blender, float percent, Size size, Point location) public static IImageProcessingContext<TPixel> DrawImage<TPixel>(this IImageProcessingContext<TPixel> source, Image<TPixel> image, PixelBlenderMode blender, float opacity, Point location)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
GraphicsOptions options = GraphicsOptions.Default; GraphicsOptions options = GraphicsOptions.Default;
options.BlenderMode = blender; options.BlenderMode = blender;
options.BlendPercentage = percent; options.BlendPercentage = opacity;
return source.DrawImage(image, size, location, options); return source.DrawImage(image, location, options);
} }
} }
} }

88
src/ImageSharp.Drawing/Processors/DrawImageProcessor.cs

@ -19,93 +19,77 @@ namespace SixLabors.ImageSharp.Drawing.Processors
internal class DrawImageProcessor<TPixel> : ImageProcessor<TPixel> internal class DrawImageProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly PixelBlender<TPixel> blender;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="DrawImageProcessor{TPixel}"/> class.
/// </summary> /// </summary>
/// <param name="image">The image to blend with the currently processing image.</param> /// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="size">The size to draw the blended image.</param>
/// <param name="location">The location to draw the blended image.</param> /// <param name="location">The location to draw the blended image.</param>
/// <param name="options">The opacity of the image to blend. Between 0 and 100.</param> /// <param name="options">The opacity of the image to blend. Between 0 and 1.</param>
public DrawImageProcessor(Image<TPixel> image, Size size, Point location, GraphicsOptions options) public DrawImageProcessor(Image<TPixel> image, Point location, GraphicsOptions options)
{ {
Guard.MustBeBetweenOrEqualTo(options.BlendPercentage, 0, 1, nameof(options.BlendPercentage)); Guard.MustBeBetweenOrEqualTo(options.BlendPercentage, 0, 1, nameof(options.BlendPercentage));
this.Image = image; this.Image = image;
this.Size = size; this.Opacity = options.BlendPercentage;
this.Alpha = options.BlendPercentage; this.Blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options.BlenderMode);
this.blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options.BlenderMode);
this.Location = location; this.Location = location;
} }
/// <summary> /// <summary>
/// Gets the image to blend. /// Gets the image to blend
/// </summary> /// </summary>
public Image<TPixel> Image { get; } public Image<TPixel> Image { get; }
/// <summary> /// <summary>
/// Gets the alpha percentage value. /// Gets the opacity of the image to blend
/// </summary> /// </summary>
public float Alpha { get; } public float Opacity { get; }
/// <summary> /// <summary>
/// Gets the size to draw the blended image. /// Gets the pixel blender
/// </summary> /// </summary>
public Size Size { get; } public PixelBlender<TPixel> Blender { get; }
/// <summary> /// <summary>
/// Gets the location to draw the blended image. /// Gets the location to draw the blended image
/// </summary> /// </summary>
public Point Location { get; } public Point Location { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{ {
Image<TPixel> disposableImage = null;
Image<TPixel> targetImage = this.Image; Image<TPixel> targetImage = this.Image;
PixelBlender<TPixel> blender = this.Blender;
int locationY = this.Location.Y;
try // Align start/end positions.
{ Rectangle bounds = targetImage.Bounds();
if (targetImage.Size() != this.Size)
{
targetImage = disposableImage = this.Image.Clone(x => x.Resize(this.Size.Width, this.Size.Height));
}
// Align start/end positions.
Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
maxX = Math.Min(this.Location.X + this.Size.Width, maxX);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y); int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X;
maxY = Math.Min(this.Location.Y + this.Size.Height, maxY); int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX; int width = maxX - minX;
using (var amount = new Buffer<float>(width)) using (var amount = new Buffer<float>(width))
{
for (int i = 0; i < width; i++)
{ {
for (int i = 0; i < width; i++) amount[i] = this.Opacity;
{
amount[i] = this.Alpha;
}
Parallel.For(
minY,
maxY,
configuration.ParallelOptions,
y =>
{
Span<TPixel> background = source.GetPixelRowSpan(y).Slice(minX, width);
Span<TPixel> foreground = targetImage.GetPixelRowSpan(y - this.Location.Y).Slice(targetX, width);
this.blender.Blend(background, background, foreground, amount);
});
} }
}
finally Parallel.For(
{ minY,
disposableImage?.Dispose(); maxY,
configuration.ParallelOptions,
y =>
{
Span<TPixel> background = source.GetPixelRowSpan(y).Slice(minX, width);
Span<TPixel> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
blender.Blend(background, background, foreground, amount);
});
} }
} }
} }

2
src/ImageSharp/GraphicsOptions.cs

@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp
// some API thought post V1. // some API thought post V1.
/// <summary> /// <summary>
/// Gets or sets a value indicating the blending percentage to apply to the drawing operation /// Gets or sets a value indicating the blending mode to apply to the drawing operation
/// </summary> /// </summary>
public PixelBlenderMode BlenderMode public PixelBlenderMode BlenderMode
{ {

5
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -51,8 +51,6 @@ namespace SixLabors.ImageSharp.Processing.Processors
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
: base(sampler) : base(sampler)
{ {
// Transforms are inverted else the output is the opposite of the expected.
Matrix3x2.Invert(matrix, out matrix);
this.TransformMatrix = matrix; this.TransformMatrix = matrix;
this.targetDimensions = targetDimensions; this.targetDimensions = targetDimensions;
} }
@ -95,6 +93,9 @@ namespace SixLabors.ImageSharp.Processing.Processors
// Since could potentially be resizing the canvas we might need to re-calculate the matrix // Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
// Convert from screen to world space.
Matrix3x2.Invert(matrix, out matrix);
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
Parallel.For( Parallel.For(

13
src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs

@ -27,23 +27,14 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/> /// <inheritdoc/>
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{ {
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F);
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix)
{ {
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix).Size;
if (!Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix))
{
// TODO: Shouldn't we throw an exception instead?
return sourceDimensions;
}
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix).Size;
} }
} }
} }

11
src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs

@ -27,17 +27,14 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/> /// <inheritdoc/>
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{ {
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0);
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix)
{ {
return Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 sizeMatrix) var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix) return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix).Size;
: sourceRectangle;
} }
} }
} }

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

@ -22,8 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel> internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
// TODO: We should use a Size instead! (See AffineTransformProcessor<T>) private Size targetDimensions;
private Rectangle targetRectangle;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
@ -40,7 +39,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <param name="matrix">The transform matrix</param> /// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param> /// <param name="sampler">The sampler to perform the transform operation.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler)
: this(matrix, sampler, Rectangle.Empty) : this(matrix, sampler, Size.Empty)
{ {
} }
@ -49,32 +48,31 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// </summary> /// </summary>
/// <param name="matrix">The transform matrix</param> /// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param> /// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param> /// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions)
: base(sampler) : base(sampler)
{ {
// Transforms are inverted else the output is the opposite of the expected.
Matrix4x4.Invert(matrix, out matrix);
this.TransformMatrix = matrix; this.TransformMatrix = matrix;
this.targetRectangle = rectangle; this.targetDimensions = targetDimensions;
} }
/// <summary> /// <summary>
/// Gets the matrix used to supply the non-affine transform /// Gets the matrix used to supply the projective transform
/// </summary> /// </summary>
public Matrix4x4 TransformMatrix { get; } public Matrix4x4 TransformMatrix { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle) protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{ {
if (this.targetRectangle == Rectangle.Empty) if (this.targetDimensions == Size.Empty)
{ {
this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix); // TODO: CreateDestination() should not modify the processors state! (kinda CQRS)
this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix);
} }
// We will always be creating the clone even for mutate because we may need to resize the canvas // We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames = IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone())); source.Frames.Select(x => new ImageFrame<TPixel>(this.targetDimensions.Width, this.targetDimensions.Height, x.MetaData.Clone()));
// Use the overload to prevent an extra frame being added // Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames); return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
@ -83,12 +81,17 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration) protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{ {
int height = this.targetRectangle.Height; int height = this.targetDimensions.Height;
int width = this.targetRectangle.Width; int width = this.targetDimensions.Width;
Rectangle sourceBounds = source.Bounds(); Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Since could potentially be resizing the canvas we might need to re-calculate the matrix // Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
// Convert from screen to world space.
Matrix4x4.Invert(matrix, out matrix);
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
@ -229,12 +232,12 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <summary> /// <summary>
/// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix. /// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="sourceRectangle">The source rectangle.</param> /// <param name="sourceDimensions">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Rectangle"/></returns> /// <returns>The <see cref="Rectangle"/></returns>
protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix)
{ {
return sourceRectangle; return sourceDimensions;
} }
} }
} }

10
src/ImageSharp/Processing/Transforms/Transform.cs

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp
/// <returns>The <see cref="Image{TPixel}"/></returns> /// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix) public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.NearestNeighbor); => Transform(source, matrix, KnownResamplers.Bicubic);
/// <summary> /// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm. /// Transforms an image by the given matrix using the specified sampling algorithm.
@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp
/// <returns>The <see cref="Image{TPixel}"/></returns> /// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix) internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.NearestNeighbor); => Transform(source, matrix, KnownResamplers.Bicubic);
/// <summary> /// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
@ -117,6 +117,10 @@ namespace SixLabors.ImageSharp
/// <returns>The <see cref="Image{TPixel}"/></returns> /// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(matrix, sampler, rectangle)); {
var t = Matrix4x4.CreateTranslation(new Vector3(-rectangle.Location, 0));
Matrix4x4 combinedMatrix = t * matrix;
return source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size));
}
} }
} }

48
src/ImageSharp/Processing/Transforms/TransformHelpers.cs

@ -59,7 +59,51 @@ namespace SixLabors.ImageSharp
} }
/// <summary> /// <summary>
/// Returns the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix. /// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F);
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix4x4.Invert(matrix, out Matrix4x4 inverted);
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0);
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0);
Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="rectangle">The source rectangle.</param> /// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
@ -79,7 +123,7 @@ namespace SixLabors.ImageSharp
} }
/// <summary> /// <summary>
/// Returns the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix. /// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="rectangle">The source rectangle.</param> /// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>

38
tests/ImageSharp.Tests/Drawing/DrawImageTest.cs

@ -1,17 +1,16 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System.IO; using System;
using System.Linq; using System.Numerics;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives; using SixLabors.Primitives;
using Xunit; using Xunit;
namespace SixLabors.ImageSharp.Tests namespace SixLabors.ImageSharp.Tests
{ {
using System;
using System.Numerics;
public class DrawImageTest : FileTestBase public class DrawImageTest : FileTestBase
{ {
private const PixelTypes PixelTypes = Tests.PixelTypes.Rgba32; private const PixelTypes PixelTypes = Tests.PixelTypes.Rgba32;
@ -23,8 +22,6 @@ namespace SixLabors.ImageSharp.Tests
TestImages.Gif.Rings TestImages.Gif.Rings
}; };
object[][] Modes = System.Enum.GetValues(typeof(PixelBlenderMode)).Cast<PixelBlenderMode>().Select(x => new object[] { x }).ToArray();
[Theory] [Theory]
[WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Normal)] [WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Normal)]
[WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Multiply)] [WithFileCollection(nameof(TestFiles), PixelTypes, PixelBlenderMode.Multiply)]
@ -39,9 +36,10 @@ namespace SixLabors.ImageSharp.Tests
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
using (Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes)) using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{ {
image.Mutate(x => x.DrawImage(blend, mode, .75f, new Size(image.Width / 2, image.Height / 2), new Point(image.Width / 4, image.Height / 4))); blend.Mutate(x => x.Resize(image.Width / 2, image.Height / 2));
image.Mutate(x => x.DrawImage(blend, mode, .75f, new Point(image.Width / 4, image.Height / 4)));
image.DebugSave(provider, new { mode }); image.DebugSave(provider, new { mode });
} }
} }
@ -52,15 +50,25 @@ namespace SixLabors.ImageSharp.Tests
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
using (Image<TPixel> blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes)) using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{ {
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F); Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F);
Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F)); Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F));
Matrix3x2 matrix = rotate * scale;
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
Rectangle srcBounds = blend.Bounds();
Rectangle destBounds = TransformHelpers.GetTransformedBoundingRectangle(srcBounds, matrix);
Matrix3x2 centeredMatrix = TransformHelpers.GetCenteredTransformMatrix(srcBounds, destBounds, matrix);
blend.Mutate(x => x.Transform(rotate * scale)); // We pass a new rectangle here based on the dest bounds since we've offset the matrix
blend.Mutate(x => x.Transform(
centeredMatrix,
KnownResamplers.Bicubic,
new Rectangle(0, 0, destBounds.Width, destBounds.Height)));
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, mode, .75F, new Size(blend.Width, blend.Height), position)); image.Mutate(x => x.DrawImage(blend, mode, .75F, position));
image.DebugSave(provider, new[] { "Transformed" }); image.DebugSave(provider, new[] { "Transformed" });
} }
} }
@ -78,7 +86,7 @@ namespace SixLabors.ImageSharp.Tests
Rgba32 backgroundPixel = background[0, 0]; Rgba32 backgroundPixel = background[0, 0];
Rgba32 overlayPixel = overlay[Math.Abs(xy) + 1, Math.Abs(xy) + 1]; Rgba32 overlayPixel = overlay[Math.Abs(xy) + 1, Math.Abs(xy) + 1];
background.Mutate(x => x.DrawImage(overlay, PixelBlenderMode.Normal, 1F, new Size(overlay.Width, overlay.Height), new Point(xy, xy))); background.Mutate(x => x.DrawImage(overlay, PixelBlenderMode.Normal, 1F, new Point(xy, xy)));
Assert.Equal(Rgba32.White, backgroundPixel); Assert.Equal(Rgba32.White, backgroundPixel);
Assert.Equal(overlayPixel, background[0, 0]); Assert.Equal(overlayPixel, background[0, 0]);
@ -100,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests
Rgba32 backgroundPixel = background[xy - 1, xy - 1]; Rgba32 backgroundPixel = background[xy - 1, xy - 1];
Rgba32 overlayPixel = overlay[0, 0]; Rgba32 overlayPixel = overlay[0, 0];
background.Mutate(x => x.DrawImage(overlay, PixelBlenderMode.Normal, 1F, new Size(overlay.Width, overlay.Height), new Point(xy, xy))); background.Mutate(x => x.DrawImage(overlay, PixelBlenderMode.Normal, 1F, new Point(xy, xy)));
Assert.Equal(Rgba32.White, backgroundPixel); Assert.Equal(Rgba32.White, backgroundPixel);
Assert.Equal(overlayPixel, background[xy, xy]); Assert.Equal(overlayPixel, background[xy, xy]);
@ -109,4 +117,4 @@ namespace SixLabors.ImageSharp.Tests
} }
} }
} }
} }

2
tests/ImageSharp.Tests/Image/PixelAccessorTests.cs

@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests
[WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Xyz)] [WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Xyz)]
[WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Zyx)] [WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Zyx)]
[WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Xyzw)] [WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Xyzw)]
[WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Zyxw)] // [WithBlankImages(16, 16, PixelTypes.All, ComponentOrder.Zyxw)] TODO: This fails sometimes on Travis. Investigate
internal void CopyToThenCopyFromWithOffset<TPixel>(TestImageProvider<TPixel> provider, ComponentOrder order) internal void CopyToThenCopyFromWithOffset<TPixel>(TestImageProvider<TPixel> provider, ComponentOrder order)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {

Loading…
Cancel
Save