Browse Source

Merge branch 'master' into js/projective-transforms

pull/546/head
James Jackson-South 8 years ago
parent
commit
4f9edfae38
  1. 21
      src/ImageSharp.Drawing/Processing/Drawing/Brushes/SolidBrush{TPixel}.cs
  2. 74
      src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs
  3. 115
      src/ImageSharp/IImageFrameCollection.cs
  4. 139
      src/ImageSharp/ImageFrameCollection.cs
  5. 59
      src/ImageSharp/ImageFrame{TPixel}.cs
  6. 37
      src/ImageSharp/Image{TPixel}.cs
  7. 24
      src/ImageSharp/PixelFormats/PixelBlenderMode.cs
  8. 92
      tests/ImageSharp.Tests/Drawing/BlendedShapes.cs
  9. 173
      tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs
  10. 154
      tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.cs
  11. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  12. 15
      tests/ImageSharp.Tests/Image/ImageFramesCollectionTests.cs
  13. 60
      tests/ImageSharp.Tests/Image/ImageTests.cs
  14. 2
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs
  15. 52
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  16. 52
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  17. 5
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

21
src/ImageSharp.Drawing/Processing/Drawing/Brushes/SolidBrush{TPixel}.cs

@ -90,16 +90,23 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
MemoryManager memoryManager = this.Target.MemoryManager; MemoryManager memoryManager = this.Target.MemoryManager;
using (IBuffer<float> amountBuffer = memoryManager.Allocate<float>(scanline.Length)) if (this.Options.BlendPercentage == 1f)
{ {
Span<float> amountSpan = amountBuffer.Span; this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, scanline);
}
for (int i = 0; i < scanline.Length; i++) else
{
using (IBuffer<float> amountBuffer = memoryManager.Allocate<float>(scanline.Length))
{ {
amountSpan[i] = scanline[i] * this.Options.BlendPercentage; Span<float> amountSpan = amountBuffer.Span;
}
for (int i = 0; i < scanline.Length; i++)
{
amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
}
this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan); this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan);
}
} }
} }
} }

74
src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Drawing.Brushes; using SixLabors.ImageSharp.Processing.Drawing.Brushes;
@ -49,39 +50,66 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Processors
int minY = Math.Max(0, startY); int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY); int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
int width = maxX - minX; int width = maxX - minX;
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width)) // If there's no reason for blending, then avoid it.
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator( if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
source,
sourceRectangle,
this.options))
{ {
amount.Span.Fill(this.options.BlendPercentage);
Parallel.For( Parallel.For(
minY, minY,
maxY, maxY,
configuration.ParallelOptions, configuration.ParallelOptions,
y => y =>
{ {
int offsetY = y - startY; source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
int offsetX = minX - startX; });
}
else
{
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width))
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source,
sourceRectangle,
this.options))
{
amount.Span.Fill(1f);
Parallel.For(
minY,
maxY,
configuration.ParallelOptions,
y =>
{
int offsetY = y - startY;
int offsetX = minX - startX;
applicator.Apply(amount.Span, offsetX, offsetY); applicator.Apply(amount.Span, offsetX, offsetY);
}); });
}
} }
} }
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
solidBrush = this.brush as SolidBrush<TPixel>;
return solidBrush != null
&& ((this.options.BlenderMode == PixelBlenderMode.Normal && this.options.BlendPercentage == 1f
&& solidBrush.Color.ToVector4().W == 1f)
|| (this.options.BlenderMode == PixelBlenderMode.Over && this.options.BlendPercentage == 1f
&& solidBrush.Color.ToVector4().W == 1f)
|| (this.options.BlenderMode == PixelBlenderMode.Src));
}
} }
} }

115
src/ImageSharp/IImageFrameCollection.cs

@ -1,115 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Encapsulates a collection of <see cref="ImageFrame{T}"/> instances that make up an <see cref="Image{T}"/>.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
public interface IImageFrameCollection<TPixel> : IEnumerable<ImageFrame<TPixel>>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets the number of frames.
/// </summary>
int Count { get; }
/// <summary>
/// Gets the root frame.
/// </summary>
ImageFrame<TPixel> RootFrame { get; }
/// <summary>
/// Gets the <see cref="ImageFrame{TPixel}"/> at the specified index.
/// </summary>
/// <value>
/// The <see cref="ImageFrame{TPixel}"/>.
/// </value>
/// <param name="index">The index.</param>
/// <returns>The <see cref="ImageFrame{TPixel}"/> at the specified index.</returns>
ImageFrame<TPixel> this[int index] { get; }
/// <summary>
/// Creates an <see cref="Image{T}"/> with only the frame at the specified index
/// with the same metadata as the original image.
/// </summary>
/// <param name="index">The zero-based index of the frame to clone.</param>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
Image<TPixel> CloneFrame(int index);
/// <summary>
/// Removes the frame at the specified index and creates a new image with only the removed frame
/// with the same metadata as the original image.
/// </summary>
/// <param name="index">The zero-based index of the frame to export.</param>
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
Image<TPixel> ExportFrame(int index);
/// <summary>
/// Removes the frame at the specified index and frees all freeable resources associated with it.
/// </summary>
/// <param name="index">The zero-based index of the frame to remove.</param>
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
void RemoveFrame(int index);
/// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}"/> and appends it to the end of the collection.
/// </summary>
/// <returns>The new <see cref="ImageFrame{TPixel}"/>.</returns>
ImageFrame<TPixel> CreateFrame();
/// <summary>
/// Clones the <paramref name="source"/> frame and appends the clone to the end of the collection.
/// </summary>
/// <param name="source">The raw pixel data to generate the <seealso cref="ImageFrame{TPixel}"/> from.</param>
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
ImageFrame<TPixel> AddFrame(ImageFrame<TPixel> source);
/// <summary>
/// Creates a new frame from the pixel data with the same dimensions as the other frames and inserts the
/// new frame at the end of the collection.
/// </summary>
/// <param name="source">The raw pixel data to generate the <seealso cref="ImageFrame{TPixel}"/> from.</param>
/// <returns>The new <see cref="ImageFrame{TPixel}"/>.</returns>
ImageFrame<TPixel> AddFrame(TPixel[] source);
/// <summary>
/// Clones and inserts the <paramref name="source"/> into the <seealso cref="IImageFrameCollection{TPixel}"/> at the specified <paramref name="index"/>.
/// </summary>
/// <param name="index">The zero-based index to insert the frame at.</param>
/// <param name="source">The <seealso cref="ImageFrame{TPixel}"/> to clone and insert into the <seealso cref="IImageFrameCollection{TPixel}"/>.</param>
/// <exception cref="ArgumentException">Frame must have the same dimensions as the image.</exception>
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
ImageFrame<TPixel> InsertFrame(int index, ImageFrame<TPixel> source);
/// <summary>
/// Moves an <seealso cref="ImageFrame{TPixel}"/> from <paramref name="sourceIndex"/> to <paramref name="destinationIndex"/>.
/// </summary>
/// <param name="sourceIndex">The zero-based index of the frame to move.</param>
/// <param name="destinationIndex">The index to move the frame to.</param>
void MoveFrame(int sourceIndex, int destinationIndex);
/// <summary>
/// Determines the index of a specific <paramref name="frame"/> in the <seealso cref="IImageFrameCollection{TPixel}"/>.
/// </summary>
/// <param name="frame">The <seealso cref="ImageFrame{TPixel}"/> to locate in the <seealso cref="IImageFrameCollection{TPixel}"/>.</param>
/// <returns>The index of item if found in the list; otherwise, -1.</returns>
int IndexOf(ImageFrame<TPixel> frame);
/// <summary>
/// Determines whether the <seealso cref="IImageFrameCollection{TPixel}"/> contains the <paramref name="frame"/>.
/// </summary>
/// <param name="frame">The frame.</param>
/// <returns>
/// <c>true</c> if the <seealso cref="IImageFrameCollection{TPixel}"/> contains the specified frame; otherwise, <c>false</c>.
/// </returns>
bool Contains(ImageFrame<TPixel> frame);
}
}

139
src/ImageSharp/ImageFrameCollection.cs

@ -9,21 +9,24 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp namespace SixLabors.ImageSharp
{ {
/// <inheritdoc/> /// <summary>
internal sealed class ImageFrameCollection<TPixel> : IImageFrameCollection<TPixel> /// Encapsulates a collection of <see cref="ImageFrame{T}"/> instances that make up an <see cref="Image{T}"/>.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
public sealed class ImageFrameCollection<TPixel> : IEnumerable<ImageFrame<TPixel>>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly IList<ImageFrame<TPixel>> frames = new List<ImageFrame<TPixel>>(); private readonly IList<ImageFrame<TPixel>> frames = new List<ImageFrame<TPixel>>();
private readonly Image<TPixel> parent; private readonly Image<TPixel> parent;
internal ImageFrameCollection(Image<TPixel> parent, int width, int height) internal ImageFrameCollection(Image<TPixel> parent, int width, int height, TPixel backgroundColor)
{ {
Guard.NotNull(parent, nameof(parent)); Guard.NotNull(parent, nameof(parent));
this.parent = parent; this.parent = parent;
// Frames are already cloned within the caller // Frames are already cloned within the caller
this.frames.Add(new ImageFrame<TPixel>(parent.GetConfiguration().MemoryManager, width, height)); this.frames.Add(new ImageFrame<TPixel>(parent.GetConfiguration(), width, height, backgroundColor));
} }
internal ImageFrameCollection(Image<TPixel> parent, IEnumerable<ImageFrame<TPixel>> frames) internal ImageFrameCollection(Image<TPixel> parent, IEnumerable<ImageFrame<TPixel>> frames)
@ -41,51 +44,85 @@ namespace SixLabors.ImageSharp
} }
} }
/// <inheritdoc/> /// <summary>
/// Gets the number of frames.
/// </summary>
public int Count => this.frames.Count; public int Count => this.frames.Count;
/// <inheritdoc/> /// <summary>
/// Gets the root frame.
/// </summary>
public ImageFrame<TPixel> RootFrame => this.frames.Count > 0 ? this.frames[0] : null; public ImageFrame<TPixel> RootFrame => this.frames.Count > 0 ? this.frames[0] : null;
/// <inheritdoc/> /// <summary>
/// Gets the <see cref="ImageFrame{TPixel}"/> at the specified index.
/// </summary>
/// <value>
/// The <see cref="ImageFrame{TPixel}"/>.
/// </value>
/// <param name="index">The index.</param>
/// <returns>The <see cref="ImageFrame{TPixel}"/> at the specified index.</returns>
public ImageFrame<TPixel> this[int index] => this.frames[index]; public ImageFrame<TPixel> this[int index] => this.frames[index];
/// <inheritdoc/> /// <summary>
/// Determines the index of a specific <paramref name="frame"/> in the <seealso cref="ImageFrameCollection{TPixel}"/>.
/// </summary>
/// <param name="frame">The <seealso cref="ImageFrame{TPixel}"/> to locate in the <seealso cref="ImageFrameCollection{TPixel}"/>.</param>
/// <returns>The index of item if found in the list; otherwise, -1.</returns>
public int IndexOf(ImageFrame<TPixel> frame) => this.frames.IndexOf(frame); public int IndexOf(ImageFrame<TPixel> frame) => this.frames.IndexOf(frame);
/// <inheritdoc/> /// <summary>
public ImageFrame<TPixel> InsertFrame(int index, ImageFrame<TPixel> frame) /// Clones and inserts the <paramref name="source"/> into the <seealso cref="ImageFrameCollection{TPixel}"/> at the specified <paramref name="index"/>.
/// </summary>
/// <param name="index">The zero-based index to insert the frame at.</param>
/// <param name="source">The <seealso cref="ImageFrame{TPixel}"/> to clone and insert into the <seealso cref="ImageFrameCollection{TPixel}"/>.</param>
/// <exception cref="ArgumentException">Frame must have the same dimensions as the image.</exception>
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> InsertFrame(int index, ImageFrame<TPixel> source)
{ {
this.ValidateFrame(frame); this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = frame.Clone(); ImageFrame<TPixel> clonedFrame = source.Clone();
this.frames.Insert(index, clonedFrame); this.frames.Insert(index, clonedFrame);
return clonedFrame; return clonedFrame;
} }
/// <inheritdoc/> /// <summary>
public ImageFrame<TPixel> AddFrame(ImageFrame<TPixel> frame) /// Clones the <paramref name="source"/> frame and appends the clone to the end of the collection.
/// </summary>
/// <param name="source">The raw pixel data to generate the <seealso cref="ImageFrame{TPixel}"/> from.</param>
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> AddFrame(ImageFrame<TPixel> source)
{ {
this.ValidateFrame(frame); this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = frame.Clone(); ImageFrame<TPixel> clonedFrame = source.Clone();
this.frames.Add(clonedFrame); this.frames.Add(clonedFrame);
return clonedFrame; return clonedFrame;
} }
/// <inheritdoc/> /// <summary>
public ImageFrame<TPixel> AddFrame(TPixel[] data) /// Creates a new frame from the pixel data with the same dimensions as the other frames and inserts the
/// new frame at the end of the collection.
/// </summary>
/// <param name="source">The raw pixel data to generate the <seealso cref="ImageFrame{TPixel}"/> from.</param>
/// <returns>The new <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> AddFrame(TPixel[] source)
{ {
Guard.NotNull(data, nameof(data)); Guard.NotNull(source, nameof(source));
var frame = ImageFrame.LoadPixelData( var frame = ImageFrame.LoadPixelData(
this.parent.GetMemoryManager(), this.parent.GetMemoryManager(),
new Span<TPixel>(data), new Span<TPixel>(source),
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height); this.RootFrame.Height);
this.frames.Add(frame); this.frames.Add(frame);
return frame; return frame;
} }
/// <inheritdoc/> /// <summary>
/// Removes the frame at the specified index and frees all freeable resources associated with it.
/// </summary>
/// <param name="index">The zero-based index of the frame to remove.</param>
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
public void RemoveFrame(int index) public void RemoveFrame(int index)
{ {
if (index == 0 && this.Count == 1) if (index == 0 && this.Count == 1)
@ -98,26 +135,42 @@ namespace SixLabors.ImageSharp
frame.Dispose(); frame.Dispose();
} }
/// <inheritdoc/> /// <summary>
/// Determines whether the <seealso cref="ImageFrameCollection{TPixel}"/> contains the <paramref name="frame"/>.
/// </summary>
/// <param name="frame">The frame.</param>
/// <returns>
/// <c>true</c> if the <seealso cref="ImageFrameCollection{TPixel}"/> contains the specified frame; otherwise, <c>false</c>.
/// </returns>
public bool Contains(ImageFrame<TPixel> frame) public bool Contains(ImageFrame<TPixel> frame)
{ {
return this.frames.Contains(frame); return this.frames.Contains(frame);
} }
/// <inheritdoc/> /// <summary>
public void MoveFrame(int sourceIndex, int destIndex) /// Moves an <seealso cref="ImageFrame{TPixel}"/> from <paramref name="sourceIndex"/> to <paramref name="destinationIndex"/>.
/// </summary>
/// <param name="sourceIndex">The zero-based index of the frame to move.</param>
/// <param name="destinationIndex">The index to move the frame to.</param>
public void MoveFrame(int sourceIndex, int destinationIndex)
{ {
if (sourceIndex == destIndex) if (sourceIndex == destinationIndex)
{ {
return; return;
} }
ImageFrame<TPixel> frameAtIndex = this.frames[sourceIndex]; ImageFrame<TPixel> frameAtIndex = this.frames[sourceIndex];
this.frames.RemoveAt(sourceIndex); this.frames.RemoveAt(sourceIndex);
this.frames.Insert(destIndex, frameAtIndex); this.frames.Insert(destinationIndex, frameAtIndex);
} }
/// <inheritdoc/> /// <summary>
/// Removes the frame at the specified index and creates a new image with only the removed frame
/// with the same metadata as the original image.
/// </summary>
/// <param name="index">The zero-based index of the frame to export.</param>
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public Image<TPixel> ExportFrame(int index) public Image<TPixel> ExportFrame(int index)
{ {
ImageFrame<TPixel> frame = this[index]; ImageFrame<TPixel> frame = this[index];
@ -132,7 +185,12 @@ namespace SixLabors.ImageSharp
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame }); return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame });
} }
/// <inheritdoc/> /// <summary>
/// Creates an <see cref="Image{T}"/> with only the frame at the specified index
/// with the same metadata as the original image.
/// </summary>
/// <param name="index">The zero-based index of the frame to clone.</param>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public Image<TPixel> CloneFrame(int index) public Image<TPixel> CloneFrame(int index)
{ {
ImageFrame<TPixel> frame = this[index]; ImageFrame<TPixel> frame = this[index];
@ -140,10 +198,31 @@ namespace SixLabors.ImageSharp
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame }); return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame });
} }
/// <inheritdoc/> /// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}" /> and appends it to the end of the collection.
/// </summary>
/// <returns>
/// The new <see cref="ImageFrame{TPixel}" />.
/// </returns>
public ImageFrame<TPixel> CreateFrame() public ImageFrame<TPixel> CreateFrame()
{ {
var frame = new ImageFrame<TPixel>(this.parent.GetConfiguration().MemoryManager, this.RootFrame.Width, this.RootFrame.Height); return this.CreateFrame(default);
}
/// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}" /> and appends it to the end of the collection.
/// </summary>
/// <param name="backgroundColor">The background color to initialize the pixels with.</param>
/// <returns>
/// The new <see cref="ImageFrame{TPixel}" />.
/// </returns>
public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor)
{
var frame = new ImageFrame<TPixel>(
this.parent.GetConfiguration(),
this.RootFrame.Width,
this.RootFrame.Height,
backgroundColor);
this.frames.Add(frame); this.frames.Add(frame);
return frame; return frame;
} }

59
src/ImageSharp/ImageFrame{TPixel}.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -18,8 +19,7 @@ namespace SixLabors.ImageSharp
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
public sealed class ImageFrame<TPixel> : IPixelSource<TPixel>, IDisposable public sealed class ImageFrame<TPixel> : IPixelSource<TPixel>, IDisposable
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel> {
{
private bool isDisposed; private bool isDisposed;
/// <summary> /// <summary>
@ -29,8 +29,7 @@ namespace SixLabors.ImageSharp
/// <param name="width">The width of the image in pixels.</param> /// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param> /// <param name="height">The height of the image in pixels.</param>
internal ImageFrame(MemoryManager memoryManager, int width, int height) internal ImageFrame(MemoryManager memoryManager, int width, int height)
: this(memoryManager, width, height, new ImageFrameMetaData()) : this(memoryManager, width, height, new ImageFrameMetaData()) {
{
} }
/// <summary> /// <summary>
@ -52,6 +51,38 @@ namespace SixLabors.ImageSharp
this.MetaData = metaData; this.MetaData = metaData;
} }
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to use for buffer allocation and parallel options to clear the buffer with.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to clear the image with.</param>
internal ImageFrame(Configuration configuration, int width, int height, TPixel backgroundColor)
: this(configuration, width, height, backgroundColor, new ImageFrameMetaData()) {
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to use for buffer allocation and parallel options to clear the buffer with.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to clear the image with.</param>
/// <param name="metaData">The meta data.</param>
internal ImageFrame(Configuration configuration, int width, int height, TPixel backgroundColor, ImageFrameMetaData metaData)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.NotNull(metaData, nameof(metaData));
this.MemoryManager = configuration.MemoryManager;
this.PixelBuffer = this.MemoryManager.Allocate2D<TPixel>(width, height, false);
this.Clear(configuration.ParallelOptions, backgroundColor);
this.MetaData = metaData;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class. /// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary> /// </summary>
@ -59,8 +90,7 @@ namespace SixLabors.ImageSharp
/// <param name="size">The <see cref="Size"/> of the frame.</param> /// <param name="size">The <see cref="Size"/> of the frame.</param>
/// <param name="metaData">The meta data.</param> /// <param name="metaData">The meta data.</param>
internal ImageFrame(MemoryManager memoryManager, Size size, ImageFrameMetaData metaData) internal ImageFrame(MemoryManager memoryManager, Size size, ImageFrameMetaData metaData)
: this(memoryManager, size.Width, size.Height, metaData) : this(memoryManager, size.Width, size.Height, metaData) {
{
} }
/// <summary> /// <summary>
@ -267,6 +297,23 @@ namespace SixLabors.ImageSharp
return target; return target;
} }
/// <summary>
/// Clears the bitmap.
/// </summary>
/// <param name="parallelOptions">The parallel options.</param>
/// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(ParallelOptions parallelOptions, TPixel value) {
Parallel.For(
0,
this.Height,
parallelOptions,
(int y) =>
{
Span<TPixel> targetRow = this.GetPixelRowSpan(y);
targetRow.Fill(value);
});
}
/// <summary> /// <summary>
/// Clones the current instance. /// Clones the current instance.
/// </summary> /// </summary>

37
src/ImageSharp/Image{TPixel}.cs

@ -37,6 +37,21 @@ namespace SixLabors.ImageSharp
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to initialize the pixels with.</param>
public Image(Configuration configuration, int width, int height, TPixel backgroundColor)
: this(configuration, width, height, backgroundColor, new ImageMetaData())
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class /// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image. /// with the height and the width of the image.
@ -63,7 +78,25 @@ namespace SixLabors.ImageSharp
this.configuration = configuration ?? Configuration.Default; this.configuration = configuration ?? Configuration.Default;
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8); this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata ?? new ImageMetaData(); this.MetaData = metadata ?? new ImageMetaData();
this.frames = new ImageFrameCollection<TPixel>(this, width, height); this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to initialize the pixels with.</param>
/// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, TPixel backgroundColor, ImageMetaData metadata) {
this.configuration = configuration ?? Configuration.Default;
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata ?? new ImageMetaData();
this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
} }
/// <summary> /// <summary>
@ -102,7 +135,7 @@ namespace SixLabors.ImageSharp
/// <summary> /// <summary>
/// Gets the frames. /// Gets the frames.
/// </summary> /// </summary>
public IImageFrameCollection<TPixel> Frames => this.frames; public ImageFrameCollection<TPixel> Frames => this.frames;
/// <summary> /// <summary>
/// Gets the root frame. /// Gets the root frame.

24
src/ImageSharp/PixelFormats/PixelBlenderMode.cs

@ -54,62 +54,62 @@ namespace SixLabors.ImageSharp.PixelFormats
HardLight, HardLight,
/// <summary> /// <summary>
/// returns the source colors /// returns the source colors.
/// </summary> /// </summary>
Src, Src,
/// <summary> /// <summary>
/// returns the source over the destination /// returns the source over the destination.
/// </summary> /// </summary>
Atop, Atop,
/// <summary> /// <summary>
/// returns the detination over the source /// returns the destination over the source.
/// </summary> /// </summary>
Over, Over,
/// <summary> /// <summary>
/// the source where the desitnation and source overlap /// The source where the destination and source overlap.
/// </summary> /// </summary>
In, In,
/// <summary> /// <summary>
/// the destination where the desitnation and source overlap /// The destination where the destination and source overlap.
/// </summary> /// </summary>
Out, Out,
/// <summary> /// <summary>
/// the destination where the source does not overlap it /// The destination where the source does not overlap it.
/// </summary> /// </summary>
Dest, Dest,
/// <summary> /// <summary>
/// the source where they dont overlap othersie dest in overlapping parts /// The source where they don't overlap othersie dest in overlapping parts.
/// </summary> /// </summary>
DestAtop, DestAtop,
/// <summary> /// <summary>
/// the destnation over the source /// The destination over the source.
/// </summary> /// </summary>
DestOver, DestOver,
/// <summary> /// <summary>
/// the destination where the desitnation and source overlap /// The destination where the destination and source overlap.
/// </summary> /// </summary>
DestIn, DestIn,
/// <summary> /// <summary>
/// the source where the desitnation and source overlap /// The source where the destination and source overlap.
/// </summary> /// </summary>
DestOut, DestOut,
/// <summary> /// <summary>
/// the clear. /// The clear.
/// </summary> /// </summary>
Clear, Clear,
/// <summary> /// <summary>
/// clear where they overlap /// Clear where they overlap.
/// </summary> /// </summary>
Xor Xor
} }

92
tests/ImageSharp.Tests/Drawing/BlendedShapes.cs

@ -1,92 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Drawing;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Drawing
{
using SixLabors.ImageSharp.Processing;
public class BlendedShapes
{
public static IEnumerable<object[]> modes = ((PixelBlenderMode[])Enum.GetValues(typeof(PixelBlenderMode)))
.Select(x => new object[] { x });
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void DrawBlendedValues<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (var img = provider.GetImage())
{
var scaleX = (img.Width / 100);
var scaleY = (img.Height / 100);
img.Mutate(x => x
.Fill(NamedColors<TPixel>.DarkBlue, new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))
.Fill(new GraphicsOptions(true) { BlenderMode = mode }, NamedColors<TPixel>.HotPink, new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)
));
img.DebugSave(provider, new { mode });
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void DrawBlendedValues_transparent<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (var img = provider.GetImage())
{
var scaleX = (img.Width / 100);
var scaleY = (img.Height / 100);
img.Mutate(x => x.Fill(NamedColors<TPixel>.DarkBlue, new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)));
img.Mutate(x => x.Fill(new GraphicsOptions(true) { BlenderMode = mode }, NamedColors<TPixel>.HotPink, new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)));
img.Mutate(x => x.Fill(new GraphicsOptions(true) { BlenderMode = mode }, NamedColors<TPixel>.Transparent, new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)));
img.DebugSave(provider, new { mode });
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void DrawBlendedValues_transparent50Percent<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (var img = provider.GetImage())
{
var scaleX = (img.Width / 100);
var scaleY = (img.Height / 100);
img.Mutate(x => x.Fill(NamedColors<TPixel>.DarkBlue, new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY)));
img.Mutate(x => x.Fill(new GraphicsOptions(true) { BlenderMode = mode }, NamedColors<TPixel>.HotPink, new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY)));
var c = NamedColors<TPixel>.Red.ToVector4();
c.W *= 0.5f;
TPixel pixel = default(TPixel);
pixel.PackFromVector4(c);
img.Mutate(x => x.Fill(new GraphicsOptions(true) { BlenderMode = mode }, pixel, new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)));
img.DebugSave(provider, new { mode });
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void DrawBlendedValues_doldidEllips<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (var img = provider.GetImage())
{
var scaleX = (img.Width / 100);
var scaleY = (img.Height / 100);
img.Mutate(x => x.Fill(NamedColors<TPixel>.DarkBlue, new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)));
img.Mutate(x => x.Fill(new GraphicsOptions(true) { BlenderMode = mode }, NamedColors<TPixel>.Black, new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)));
img.DebugSave(provider, new { mode });
}
}
}
}

173
tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs

@ -1,79 +1,164 @@
// 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.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Drawing; using SixLabors.ImageSharp.Processing.Drawing;
using SixLabors.ImageSharp.Processing.Overlays; using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.Shapes;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Drawing namespace SixLabors.ImageSharp.Tests.Drawing
{ {
public class FillSolidBrushTests : FileTestBase
[GroupOutput("Drawing")]
public class FillSolidBrushTests
{ {
[Fact] [Theory]
public void ImageShouldBeFloodFilledWithColorOnDefaultBackground() [WithBlankImages(1, 1, PixelTypes.Rgba32)]
[WithBlankImages(7, 4, PixelTypes.Rgba32)]
[WithBlankImages(16, 7, PixelTypes.Rgba32)]
[WithBlankImages(33, 32, PixelTypes.Rgba32)]
[WithBlankImages(400, 500, PixelTypes.Rgba32)]
public void DoesNotDependOnSize<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{ {
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); using (Image<TPixel> image = provider.GetImage())
using (var image = new Image<Rgba32>(500, 500))
{ {
image.Mutate(x => x.Fill(Rgba32.HotPink)); TPixel color = NamedColors<TPixel>.HotPink;
image.Save($"{path}/DefaultBack.png"); image.Mutate(c => c.Fill(color));
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
{
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]);
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); image.DebugSave(provider, appendPixelTypeToFileName: false);
} image.ComparePixelBufferTo(color);
} }
} }
[Fact] [Theory]
public void ImageShouldBeFloodFilledWithColor() [WithBlankImages(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)]
public void DoesNotDependOnSinglePixelType<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{ {
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); using (Image<TPixel> image = provider.GetImage())
using (var image = new Image<Rgba32>(500, 500))
{ {
image.Mutate(x => x TPixel color = NamedColors<TPixel>.HotPink;
.BackgroundColor(Rgba32.Blue) image.Mutate(c => c.Fill(color));
.Fill(Rgba32.HotPink));
image.Save($"{path}/Simple.png");
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) image.DebugSave(provider, appendSourceFileOrDescription: false);
{ image.ComparePixelBufferTo(color);
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]);
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]);
}
} }
} }
[Fact] [Theory]
public void ImageShouldBeFloodFilledWithColorOpacity() [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")]
[WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")]
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(TestImageProvider<TPixel> provider, string newColorName)
where TPixel : struct, IPixel<TPixel>
{ {
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); using (Image<TPixel> image = provider.GetImage())
using (var image = new Image<Rgba32>(500, 500))
{ {
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName);
image.Mutate(c => c.Fill(color));
image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
image.ComparePixelBufferTo(color);
}
}
public static readonly TheoryData<bool, string, float, PixelBlenderMode, float> BlendData =
new TheoryData<bool, string, float, PixelBlenderMode, float>()
{
{ false, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f },
{ false, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f },
{ false, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f },
{ false, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f },
{ false, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f },
{ false, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f },
{ false, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f },
{ false, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f },
{ false, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f },
{ false, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f },
{ false, "Green", 0.5f, PixelBlenderMode.Add, 0.3f },
{ false, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f },
image.Mutate(x => x { true, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f },
.BackgroundColor(Rgba32.Blue) { true, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f },
.Fill(color)); { true, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f },
image.Save($"{path}/Opacity.png"); { true, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f },
//shift background color towards forground color by the opacity amount { true, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f },
var mergedColor = new Rgba32(Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); { true, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f },
{ true, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f },
{ true, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f },
{ true, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f },
{ true, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f },
{ true, "Green", 0.5f, PixelBlenderMode.Add, 0.3f },
{ true, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f },
};
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) [Theory]
[WithSolidFilledImages(nameof(BlendData), 16, 16, "Red", PixelTypes.Rgba32)]
public void BlendFillColorOverBackround<TPixel>(
TestImageProvider<TPixel> provider,
bool triggerFillRegion,
string newColorName,
float alpha,
PixelBlenderMode blenderMode,
float blendPercentage)
where TPixel : struct, IPixel<TPixel>
{
var vec = TestUtils.GetPixelOfNamedColor<RgbaVector>(newColorName).ToVector4();
vec.W = alpha;
TPixel fillColor = default;
fillColor.PackFromVector4(vec);
using (Image<TPixel> image = provider.GetImage())
{
TPixel bgColor = image[0, 0];
var options = new GraphicsOptions(false)
{
BlenderMode = blenderMode,
BlendPercentage = blendPercentage
};
if (triggerFillRegion)
{
var region = new ShapeRegion(new RectangularPolygon(0, 0, 16, 16));
image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor), region));
}
else
{ {
Assert.Equal(mergedColor, sourcePixels[9, 9]); image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor)));
Assert.Equal(mergedColor, sourcePixels[199, 149]);
} }
var testOutputDetails = new
{
triggerFillRegion = triggerFillRegion,
newColorName = newColorName,
alpha = alpha,
blenderMode = blenderMode,
blendPercentage = blendPercentage
};
image.DebugSave(
provider,
testOutputDetails,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(blenderMode);
TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage);
image.ComparePixelBufferTo(expectedPixel);
} }
} }
} }
} }

154
tests/ImageSharp.Tests/Drawing/SolidFillBlendedShapesTests.cs

@ -0,0 +1,154 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Drawing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Drawing
{
[GroupOutput("Drawing")]
public class SolidFillBlendedShapesTests
{
public static IEnumerable<object[]> modes =
((PixelBlenderMode[])Enum.GetValues(typeof(PixelBlenderMode))).Select(x => new object[] { x });
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void _1DarkBlueRect_2BlendHotPinkRect<TPixel>(
TestImageProvider<TPixel> provider,
PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
int scaleX = img.Width / 100;
int scaleY = img.Height / 100;
img.Mutate(
x => x.Fill(
NamedColors<TPixel>.DarkBlue,
new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)
)
.Fill(new GraphicsOptions(true) { BlenderMode = mode },
NamedColors<TPixel>.HotPink,
new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))
);
VerifyImage(provider, mode, img);
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void _1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse<TPixel>(
TestImageProvider<TPixel> provider,
PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
int scaleX = img.Width / 100;
int scaleY = img.Height / 100;
img.Mutate(
x => x.Fill(
NamedColors<TPixel>.DarkBlue,
new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)));
img.Mutate(
x => x.Fill(
new GraphicsOptions(true) { BlenderMode = mode },
NamedColors<TPixel>.HotPink,
new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)));
img.Mutate(
x => x.Fill(
new GraphicsOptions(true) { BlenderMode = mode },
NamedColors<TPixel>.Transparent,
new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))
);
VerifyImage(provider, mode, img);
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void _1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse<TPixel>(
TestImageProvider<TPixel> provider,
PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
int scaleX = (img.Width / 100);
int scaleY = (img.Height / 100);
img.Mutate(
x => x.Fill(
NamedColors<TPixel>.DarkBlue,
new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY)));
img.Mutate(
x => x.Fill(
new GraphicsOptions(true) { BlenderMode = mode },
NamedColors<TPixel>.HotPink,
new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY)));
var c = NamedColors<TPixel>.Red.ToVector4();
c.W *= 0.5f;
var pixel = default(TPixel);
pixel.PackFromVector4(c);
img.Mutate(
x => x.Fill(
new GraphicsOptions(true) { BlenderMode = mode },
pixel,
new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))
);
VerifyImage(provider, mode, img); ;
}
}
[Theory]
[WithBlankImages(nameof(modes), 250, 250, PixelTypes.Rgba32)]
public void _1DarkBlueRect_2BlendBlackEllipse<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
int scaleX = (img.Width / 100);
int scaleY = (img.Height / 100);
img.Mutate(
x => x.Fill(
NamedColors<TPixel>.DarkBlue,
new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)));
img.Mutate(
x => x.Fill(
new GraphicsOptions(true) { BlenderMode = mode },
NamedColors<TPixel>.Black,
new Shapes.EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)));
VerifyImage(provider, mode, img);
}
}
private static void VerifyImage<TPixel>(TestImageProvider<TPixel> provider, PixelBlenderMode mode, Image<TPixel> img)
where TPixel : struct, IPixel<TPixel>
{
img.DebugSave(
provider,
new { mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
var comparer = ImageComparer.TolerantPercentage(0.01f, 3);
img.CompareFirstFrameToReferenceOutput(comparer,
provider,
new { mode },
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
}
}

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -153,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests
} }
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
string referenceOutputFile = ((ITestImageProvider)provider).Utility.GetReferenceOutputFileName("png", debugInfo, appendPixelType); string referenceOutputFile = ((ITestImageProvider)provider).Utility.GetReferenceOutputFileName("png", debugInfo, appendPixelType, true);
using (var actualImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder)) using (var actualImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
using (var referenceImage = Image.Load<TPixel>(referenceOutputFile, referenceDecoder)) using (var referenceImage = Image.Load<TPixel>(referenceOutputFile, referenceDecoder))

15
tests/ImageSharp.Tests/Image/ImageFramesCollectionTests.cs

@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US"); System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
this.image = new Image<Rgba32>(10, 10); this.image = new Image<Rgba32>(10, 10);
this.collection = new ImageFrameCollection<Rgba32>(this.image, 10, 10); this.collection = new ImageFrameCollection<Rgba32>(this.image, 10, 10, default);
} }
[Fact] [Fact]
@ -225,10 +225,21 @@ namespace SixLabors.ImageSharp.Tests
} }
[Fact] [Fact]
public void CreateFrame() public void CreateFrame_Default()
{ {
this.image.Frames.CreateFrame(); this.image.Frames.CreateFrame();
Assert.Equal(2, this.image.Frames.Count);
this.image.Frames[1].ComparePixelBufferTo(default(Rgba32));
}
[Fact]
public void CreateFrame_CustomFillColor()
{
this.image.Frames.CreateFrame(Rgba32.HotPink);
Assert.Equal(2, this.image.Frames.Count); Assert.Equal(2, this.image.Frames.Count);
this.image.Frames[1].ComparePixelBufferTo(Rgba32.HotPink);
} }
[Fact] [Fact]

60
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -13,10 +13,60 @@ namespace SixLabors.ImageSharp.Tests
/// <summary> /// <summary>
/// Tests the <see cref="Image"/> class. /// Tests the <see cref="Image"/> class.
/// </summary> /// </summary>
public class ImageTests : FileTestBase public class ImageTests
{ {
public class Constructor
{
[Fact]
public void Width_Height()
{
using (var image = new Image<Rgba32>(11, 23))
{
Assert.Equal(11, image.Width);
Assert.Equal(23, image.Height);
Assert.Equal(11*23, image.GetPixelSpan().Length);
image.ComparePixelBufferTo(default(Rgba32));
Assert.Equal(Configuration.Default, image.GetConfiguration());
}
}
[Fact]
public void Configuration_Width_Height()
{
Configuration configuration = Configuration.Default.ShallowCopy();
using (var image = new Image<Rgba32>(configuration, 11, 23))
{
Assert.Equal(11, image.Width);
Assert.Equal(23, image.Height);
Assert.Equal(11 * 23, image.GetPixelSpan().Length);
image.ComparePixelBufferTo(default(Rgba32));
Assert.Equal(configuration, image.GetConfiguration());
}
}
[Fact]
public void Configuration_Width_Height_BackroundColor()
{
Configuration configuration = Configuration.Default.ShallowCopy();
Rgba32 color = Rgba32.Aquamarine;
using (var image = new Image<Rgba32>(configuration, 11, 23, color))
{
Assert.Equal(11, image.Width);
Assert.Equal(23, image.Height);
Assert.Equal(11 * 23, image.GetPixelSpan().Length);
image.ComparePixelBufferTo(color);
Assert.Equal(configuration, image.GetConfiguration());
}
}
}
[Fact] [Fact]
public void ConstructorByteArray() public void Load_ByteArray()
{ {
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
@ -32,7 +82,7 @@ namespace SixLabors.ImageSharp.Tests
} }
[Fact] [Fact]
public void ConstructorFileSystem() public void Load_FileSystemPath()
{ {
TestFile file = TestFile.Create(TestImages.Bmp.Car); TestFile file = TestFile.Create(TestImages.Bmp.Car);
using (Image<Rgba32> image = Image.Load<Rgba32>(file.FullPath)) using (Image<Rgba32> image = Image.Load<Rgba32>(file.FullPath))
@ -43,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests
} }
[Fact] [Fact]
public void ConstructorFileSystem_FileNotFound() public void Load_FileSystemPath_FileNotFound()
{ {
System.IO.FileNotFoundException ex = Assert.Throws<System.IO.FileNotFoundException>( System.IO.FileNotFoundException ex = Assert.Throws<System.IO.FileNotFoundException>(
() => () =>
@ -53,7 +103,7 @@ namespace SixLabors.ImageSharp.Tests
} }
[Fact] [Fact]
public void ConstructorFileSystem_NullPath() public void Load_FileSystemPath_NullPath()
{ {
ArgumentNullException ex = Assert.Throws<ArgumentNullException>( ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
() => () =>

2
tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs

@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests
{ {
Guard.NotNull(colorName, nameof(colorName)); Guard.NotNull(colorName, nameof(colorName));
var c = (Rgba32)typeof(Rgba32).GetTypeInfo().GetField(colorName).GetValue(null); Rgba32 c = TestUtils.GetPixelOfNamedColor<Rgba32>(colorName);
this.R = c.R; this.R = c.R;
this.G = c.G; this.G = c.G;
this.B = c.B; this.B = c.B;

52
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -41,16 +41,20 @@ namespace SixLabors.ImageSharp.Tests
/// </summary> /// </summary>
public string TestName { get; set; } = string.Empty; public string TestName { get; set; } = string.Empty;
private string GetTestOutputFileNameImpl(string extension, string details, bool appendPixelTypeToFileName) private string GetTestOutputFileNameImpl(
string extension,
string details,
bool appendPixelTypeToFileName,
bool appendSourceFileOrDescription)
{ {
string fn = string.Empty;
if (string.IsNullOrWhiteSpace(extension)) if (string.IsNullOrWhiteSpace(extension))
{ {
extension = null; extension = null;
} }
fn = Path.GetFileNameWithoutExtension(this.SourceFileOrDescription); string fn = appendSourceFileOrDescription
? Path.GetFileNameWithoutExtension(this.SourceFileOrDescription)
: "";
if (string.IsNullOrWhiteSpace(extension)) if (string.IsNullOrWhiteSpace(extension))
{ {
@ -92,20 +96,24 @@ namespace SixLabors.ImageSharp.Tests
} }
private static string Inv(FormattableString formattable) => System.FormattableString.Invariant(formattable); private static string Inv(FormattableString formattable) => System.FormattableString.Invariant(formattable);
/// <summary> /// <summary>
/// Gets the recommended file name for the output of the test /// Gets the recommended file name for the output of the test
/// </summary> /// </summary>
/// <param name="extension">The required extension</param> /// <param name="extension">The required extension</param>
/// <param name="testOutputDetails">The settings modifying the output path</param> /// <param name="testOutputDetails">The settings modifying the output path</param>
/// <param name="appendPixelTypeToFileName">A boolean indicating whether to append the pixel type to output file name.</param> /// <param name="appendPixelTypeToFileName">A boolean indicating whether to append the pixel type to output file name.</param>
/// <param name="appendSourceFileOrDescription">A boolean indicating whether to append <see cref="ITestImageProvider.SourceFileOrDescription"/> to the test output file name.</param>
/// <returns>The file test name</returns> /// <returns>The file test name</returns>
public string GetTestOutputFileName(string extension = null, object testOutputDetails = null, bool appendPixelTypeToFileName = true) public string GetTestOutputFileName(
string extension = null,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
{ {
string detailsString = null; string detailsString = null;
string s = testOutputDetails as string;
if (s != null) if (testOutputDetails is string s)
{ {
detailsString = s; detailsString = s;
} }
@ -128,7 +136,12 @@ namespace SixLabors.ImageSharp.Tests
); );
} }
} }
return this.GetTestOutputFileNameImpl(extension, detailsString, appendPixelTypeToFileName);
return this.GetTestOutputFileNameImpl(
extension,
detailsString,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
} }
@ -139,15 +152,22 @@ namespace SixLabors.ImageSharp.Tests
/// <param name="image">The image instance</param> /// <param name="image">The image instance</param>
/// <param name="extension">The requested extension</param> /// <param name="extension">The requested extension</param>
/// <param name="encoder">Optional encoder</param> /// <param name="encoder">Optional encoder</param>
/// /// <param name="appendSourceFileOrDescription">A boolean indicating whether to append <see cref="ITestImageProvider.SourceFileOrDescription"/> to the test output file name.</param>
public string SaveTestOutputFile<TPixel>( public string SaveTestOutputFile<TPixel>(
Image<TPixel> image, Image<TPixel> image,
string extension = null, string extension = null,
IImageEncoder encoder = null, IImageEncoder encoder = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
string path = this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName); string path = this.GetTestOutputFileName(
extension,
testOutputDetails,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
encoder = encoder ?? TestEnvironment.GetReferenceEncoder(path); encoder = encoder ?? TestEnvironment.GetReferenceEncoder(path);
using (FileStream stream = File.OpenWrite(path)) using (FileStream stream = File.OpenWrite(path))
@ -161,9 +181,10 @@ namespace SixLabors.ImageSharp.Tests
int frameCount, int frameCount,
string extension = null, string extension = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
{ {
string baseDir = this.GetTestOutputFileName("", testOutputDetails, appendPixelTypeToFileName); string baseDir = this.GetTestOutputFileName("", testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription);
if (!Directory.Exists(baseDir)) if (!Directory.Exists(baseDir))
{ {
@ -211,10 +232,11 @@ namespace SixLabors.ImageSharp.Tests
internal string GetReferenceOutputFileName( internal string GetReferenceOutputFileName(
string extension, string extension,
object testOutputDetails, object testOutputDetails,
bool appendPixelTypeToFileName) bool appendPixelTypeToFileName,
bool appendSourceFileOrDescription)
{ {
return TestEnvironment.GetReferenceOutputFileName( return TestEnvironment.GetReferenceOutputFileName(
this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName) this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription)
); );
} }

52
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -61,12 +61,14 @@ namespace SixLabors.ImageSharp.Tests
/// <param name="testOutputDetails">Details to be concatenated to the test output file, describing the parameters of the test.</param> /// <param name="testOutputDetails">Details to be concatenated to the test output file, describing the parameters of the test.</param>
/// <param name="extension">The extension</param> /// <param name="extension">The extension</param>
/// <param name="appendPixelTypeToFileName">A boolean indicating whether to append the pixel type to the output file name.</param> /// <param name="appendPixelTypeToFileName">A boolean indicating whether to append the pixel type to the output file name.</param>
/// <param name="appendSourceFileOrDescription">A boolean indicating whether to append <see cref="ITestImageProvider.SourceFileOrDescription"/> to the test output file name.</param>
public static Image<TPixel> DebugSave<TPixel>( public static Image<TPixel> DebugSave<TPixel>(
this Image<TPixel> image, this Image<TPixel> image,
ITestImageProvider provider, ITestImageProvider provider,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
if (TestEnvironment.RunsOnCI) if (TestEnvironment.RunsOnCI)
@ -79,7 +81,8 @@ namespace SixLabors.ImageSharp.Tests
image, image,
extension, extension,
testOutputDetails: testOutputDetails, testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName); appendPixelTypeToFileName: appendPixelTypeToFileName,
appendSourceFileOrDescription: appendSourceFileOrDescription);
return image; return image;
} }
@ -210,7 +213,8 @@ namespace SixLabors.ImageSharp.Tests
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool grayscale = false, bool grayscale = false,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (var firstFrameOnlyImage = new Image<TPixel>(image.Width, image.Height)) using (var firstFrameOnlyImage = new Image<TPixel>(image.Width, image.Height))
@ -218,7 +222,8 @@ namespace SixLabors.ImageSharp.Tests
provider, provider,
testOutputDetails, testOutputDetails,
extension, extension,
appendPixelTypeToFileName)) appendPixelTypeToFileName,
appendSourceFileOrDescription))
{ {
firstFrameOnlyImage.Frames.AddFrame(image.Frames.RootFrame); firstFrameOnlyImage.Frames.AddFrame(image.Frames.RootFrame);
firstFrameOnlyImage.Frames.RemoveFrame(0); firstFrameOnlyImage.Frames.RemoveFrame(0);
@ -255,10 +260,15 @@ namespace SixLabors.ImageSharp.Tests
public static Image<TPixel> GetReferenceOutputImage<TPixel>(this ITestImageProvider provider, public static Image<TPixel> GetReferenceOutputImage<TPixel>(this ITestImageProvider provider,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
string referenceOutputFile = provider.Utility.GetReferenceOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName); string referenceOutputFile = provider.Utility.GetReferenceOutputFileName(
extension,
testOutputDetails,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
if (!File.Exists(referenceOutputFile)) if (!File.Exists(referenceOutputFile))
{ {
@ -336,18 +346,42 @@ namespace SixLabors.ImageSharp.Tests
Span<TPixel> expectedPixels) Span<TPixel> expectedPixels)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
Span<TPixel> actual = image.GetPixelSpan(); Span<TPixel> actualPixels = image.GetPixelSpan();
Assert.True(expectedPixels.Length == actual.Length, "Buffer sizes are not equal!"); Assert.True(expectedPixels.Length == actualPixels.Length, "Buffer sizes are not equal!");
for (int i = 0; i < expectedPixels.Length; i++) for (int i = 0; i < expectedPixels.Length; i++)
{ {
Assert.True(expectedPixels[i].Equals(actual[i]), $"Pixels are different on position {i}!"); Assert.True(expectedPixels[i].Equals(actualPixels[i]), $"Pixels are different on position {i}!");
} }
return image; return image;
} }
public static Image<TPixel> ComparePixelBufferTo<TPixel>(this Image<TPixel> image, TPixel expectedPixel)
where TPixel : struct, IPixel<TPixel>
{
foreach (ImageFrame<TPixel> imageFrame in image.Frames)
{
imageFrame.ComparePixelBufferTo(expectedPixel);
}
return image;
}
public static ImageFrame<TPixel> ComparePixelBufferTo<TPixel>(this ImageFrame<TPixel> imageFrame, TPixel expectedPixel)
where TPixel : struct, IPixel<TPixel>
{
Span<TPixel> actualPixels = imageFrame.GetPixelSpan();
for (int i = 0; i < actualPixels.Length; i++)
{
Assert.True(expectedPixel.Equals(actualPixels[i]), $"Pixels are different on position {i}!");
}
return imageFrame;
}
public static ImageFrame<TPixel> ComparePixelBufferTo<TPixel>( public static ImageFrame<TPixel> ComparePixelBufferTo<TPixel>(
this ImageFrame<TPixel> image, this ImageFrame<TPixel> image,
Span<TPixel> expectedPixels) Span<TPixel> expectedPixels)

5
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -147,6 +147,11 @@ namespace SixLabors.ImageSharp.Tests
/// <returns>The pixel types</returns> /// <returns>The pixel types</returns>
internal static PixelTypes[] GetAllPixelTypes() => (PixelTypes[])Enum.GetValues(typeof(PixelTypes)); internal static PixelTypes[] GetAllPixelTypes() => (PixelTypes[])Enum.GetValues(typeof(PixelTypes));
internal static TPixel GetPixelOfNamedColor<TPixel>(string colorName)
where TPixel : struct, IPixel<TPixel>
{
return (TPixel)typeof(NamedColors<TPixel>).GetTypeInfo().GetField(colorName).GetValue(null);
}
/// <summary> /// <summary>
/// Utility for testing image processor extension methods: /// Utility for testing image processor extension methods:

Loading…
Cancel
Save