mirror of https://github.com/SixLabors/ImageSharp
107 changed files with 3299 additions and 3308 deletions
@ -0,0 +1,2 @@ |
|||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> |
||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=processing_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> |
||||
@ -1,137 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing.Processors.Drawing; |
|
||||
using SixLabors.Primitives; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Processing |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Adds extensions that allow the drawing of images to the <see cref="Image{TPixel}"/> type.
|
|
||||
/// </summary>
|
|
||||
public static class DrawImageExtensions |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, Point.Empty, GraphicsOptions.Default.ColorBlendingMode, GraphicsOptions.Default.AlphaCompositionMode, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="colorBlending">The blending mode.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, PixelColorBlendingMode colorBlending, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, Point.Empty, colorBlending, GraphicsOptions.Default.AlphaCompositionMode, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="colorBlending">The color blending mode.</param>
|
|
||||
/// <param name="alphaComposition">The alpha composition mode.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, PixelColorBlendingMode colorBlending, PixelAlphaCompositionMode alphaComposition, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, Point.Empty, colorBlending, alphaComposition, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="options">The options, including the blending type and blending amount.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, GraphicsOptions options) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, Point.Empty, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="location">The location to draw the blended image.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, Point location, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, location, GraphicsOptions.Default.ColorBlendingMode, GraphicsOptions.Default.AlphaCompositionMode, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="location">The location to draw the blended image.</param>
|
|
||||
/// <param name="colorBlending">The color blending to apply.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlending, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, location, colorBlending, GraphicsOptions.Default.AlphaCompositionMode, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="location">The location to draw the blended image.</param>
|
|
||||
/// <param name="colorBlending">The color blending to apply.</param>
|
|
||||
/// <param name="alphaComposition">The alpha composition mode.</param>
|
|
||||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlending, PixelAlphaCompositionMode alphaComposition, float opacity) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, location, colorBlending, alphaComposition, opacity)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the given image together with the current one by blending their pixels.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixelDst">The pixel format of the destination image.</typeparam>
|
|
||||
/// <typeparam name="TPixelSrc">The pixel format of the source image.</typeparam>
|
|
||||
/// <param name="source">The image this method extends.</param>
|
|
||||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|
||||
/// <param name="location">The location to draw the blended image.</param>
|
|
||||
/// <param name="options">The options containing the blend mode and opacity.</param>
|
|
||||
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
|
||||
public static IImageProcessingContext<TPixelDst> DrawImage<TPixelDst, TPixelSrc>(this IImageProcessingContext<TPixelDst> source, Image<TPixelSrc> image, Point location, GraphicsOptions options) |
|
||||
where TPixelDst : struct, IPixel<TPixelDst> |
|
||||
where TPixelSrc : struct, IPixel<TPixelSrc> |
|
||||
=> source.ApplyProcessor(new DrawImageProcessor<TPixelDst, TPixelSrc>(image, location, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage)); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,22 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing |
||||
|
{ |
||||
|
internal static class DrawingHelpers |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Convert a <see cref="DenseMatrix{Color}"/> to a <see cref="DenseMatrix{T}"/> of the given pixel type.
|
||||
|
/// </summary>
|
||||
|
public static DenseMatrix<TPixel> ToPixelMatrix<TPixel>(this DenseMatrix<Color> colorMatrix, Configuration configuration) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
DenseMatrix<TPixel> result = new DenseMatrix<TPixel>(colorMatrix.Columns, colorMatrix.Rows); |
||||
|
Color.ToPixel(configuration, colorMatrix.Span, result.Span); |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing.Processors.Drawing; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Adds extensions that allow the drawing of images to the <see cref="Image{TPixel}"/> type.
|
||||
|
/// </summary>
|
||||
|
public static class DrawImageExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
Point.Empty, |
||||
|
GraphicsOptions.Default.ColorBlendingMode, |
||||
|
GraphicsOptions.Default.AlphaCompositionMode, |
||||
|
opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="colorBlending">The blending mode.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
PixelColorBlendingMode colorBlending, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
Point.Empty, |
||||
|
colorBlending, |
||||
|
GraphicsOptions.Default.AlphaCompositionMode, |
||||
|
opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="colorBlending">The color blending mode.</param>
|
||||
|
/// <param name="alphaComposition">The alpha composition mode.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
PixelColorBlendingMode colorBlending, |
||||
|
PixelAlphaCompositionMode alphaComposition, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor(new DrawImageProcessor(image, Point.Empty, colorBlending, alphaComposition, opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="options">The options, including the blending type and blending amount.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
GraphicsOptions options) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
Point.Empty, |
||||
|
options.ColorBlendingMode, |
||||
|
options.AlphaCompositionMode, |
||||
|
options.BlendPercentage)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="location">The location to draw the blended image.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
Point location, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
location, |
||||
|
GraphicsOptions.Default.ColorBlendingMode, |
||||
|
GraphicsOptions.Default.AlphaCompositionMode, |
||||
|
opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="location">The location to draw the blended image.</param>
|
||||
|
/// <param name="colorBlending">The color blending to apply.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
Point location, |
||||
|
PixelColorBlendingMode colorBlending, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
location, |
||||
|
colorBlending, |
||||
|
GraphicsOptions.Default.AlphaCompositionMode, |
||||
|
opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="location">The location to draw the blended image.</param>
|
||||
|
/// <param name="colorBlending">The color blending to apply.</param>
|
||||
|
/// <param name="alphaComposition">The alpha composition mode.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
Point location, |
||||
|
PixelColorBlendingMode colorBlending, |
||||
|
PixelAlphaCompositionMode alphaComposition, |
||||
|
float opacity) => |
||||
|
source.ApplyProcessor(new DrawImageProcessor(image, location, colorBlending, alphaComposition, opacity)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the given image together with the current one by blending their pixels.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image this method extends.</param>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="location">The location to draw the blended image.</param>
|
||||
|
/// <param name="options">The options containing the blend mode and opacity.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixelDst}"/>.</returns>
|
||||
|
public static IImageProcessingContext DrawImage( |
||||
|
this IImageProcessingContext source, |
||||
|
Image image, |
||||
|
Point location, |
||||
|
GraphicsOptions options) => |
||||
|
source.ApplyProcessor( |
||||
|
new DrawImageProcessor( |
||||
|
image, |
||||
|
location, |
||||
|
options.ColorBlendingMode, |
||||
|
options.AlphaCompositionMode, |
||||
|
options.BlendPercentage)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.ParallelUtils; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Combines two images together by blending the pixels.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixelBg">The pixel format of destination image.</typeparam>
|
||||
|
/// <typeparam name="TPixelFg">The pixel format of source image.</typeparam>
|
||||
|
internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg> |
||||
|
where TPixelBg : struct, IPixel<TPixelBg> |
||||
|
where TPixelFg : struct, IPixel<TPixelFg> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="image">The image to blend with the currently processing image.</param>
|
||||
|
/// <param name="location">The location to draw the blended image.</param>
|
||||
|
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
|
||||
|
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
|
||||
|
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
||||
|
public DrawImageProcessor( |
||||
|
Image<TPixelFg> image, |
||||
|
Point location, |
||||
|
PixelColorBlendingMode colorBlendingMode, |
||||
|
PixelAlphaCompositionMode alphaCompositionMode, |
||||
|
float opacity) |
||||
|
{ |
||||
|
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); |
||||
|
|
||||
|
this.Image = image; |
||||
|
this.Opacity = opacity; |
||||
|
this.Blender = PixelOperations<TPixelBg>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); |
||||
|
this.Location = location; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the image to blend
|
||||
|
/// </summary>
|
||||
|
public Image<TPixelFg> Image { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the opacity of the image to blend
|
||||
|
/// </summary>
|
||||
|
public float Opacity { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the pixel blender
|
||||
|
/// </summary>
|
||||
|
public PixelBlender<TPixelBg> Blender { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the location to draw the blended image
|
||||
|
/// </summary>
|
||||
|
public Point Location { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply( |
||||
|
ImageFrame<TPixelBg> source, |
||||
|
Rectangle sourceRectangle, |
||||
|
Configuration configuration) |
||||
|
{ |
||||
|
Image<TPixelFg> targetImage = this.Image; |
||||
|
PixelBlender<TPixelBg> blender = this.Blender; |
||||
|
int locationY = this.Location.Y; |
||||
|
|
||||
|
// 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.Right); |
||||
|
int targetX = minX - this.Location.X; |
||||
|
|
||||
|
int minY = Math.Max(this.Location.Y, sourceRectangle.Y); |
||||
|
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); |
||||
|
|
||||
|
int width = maxX - minX; |
||||
|
|
||||
|
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); |
||||
|
|
||||
|
// not a valid operation because rectangle does not overlap with this image.
|
||||
|
if (workingRect.Width <= 0 || workingRect.Height <= 0) |
||||
|
{ |
||||
|
throw new ImageProcessingException( |
||||
|
"Cannot draw image because the source image does not overlap the target image."); |
||||
|
} |
||||
|
|
||||
|
ParallelHelper.IterateRows( |
||||
|
workingRect, |
||||
|
configuration, |
||||
|
rows => |
||||
|
{ |
||||
|
for (int y = rows.Min; y < rows.Max; y++) |
||||
|
{ |
||||
|
Span<TPixelBg> background = source.GetPixelRowSpan(y).Slice(minX, width); |
||||
|
Span<TPixelFg> foreground = |
||||
|
targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); |
||||
|
blender.Blend<TPixelFg>(configuration, background, background, foreground, this.Opacity); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,124 +1,45 @@ |
|||||
// 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; |
|
||||
using System.Buffers; |
|
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using SixLabors.ImageSharp.Advanced; |
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.ParallelUtils; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
using SixLabors.ImageSharp.PixelFormats; |
||||
using SixLabors.Memory; |
using SixLabors.Memory; |
||||
using SixLabors.Primitives; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Using the brush as a source of pixels colors blends the brush color with source.
|
/// Defines a processor to fill an <see cref="Image"/> with the given <see cref="IBrush"/>
|
||||
|
/// using blending defined by the given <see cref="GraphicsOptions"/>.
|
||||
/// </summary>
|
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
public class FillProcessor : IImageProcessor |
||||
internal class FillProcessor<TPixel> : ImageProcessor<TPixel> |
|
||||
where TPixel : struct, IPixel<TPixel> |
|
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// The brush.
|
/// Initializes a new instance of the <see cref="FillProcessor"/> class.
|
||||
/// </summary>
|
/// </summary>
|
||||
private readonly IBrush<TPixel> brush; |
/// <param name="brush">The brush to use for filling.</param>
|
||||
private readonly GraphicsOptions options; |
/// <param name="options">The <see cref="GraphicsOptions"/> defining how to blend the brush pixels over the image pixels.</param>
|
||||
|
public FillProcessor(IBrush brush, GraphicsOptions options) |
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="brush">The brush to source pixel colors from.</param>
|
|
||||
/// <param name="options">The options</param>
|
|
||||
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options) |
|
||||
{ |
{ |
||||
this.brush = brush; |
this.Brush = brush; |
||||
this.options = options; |
this.Options = options; |
||||
} |
} |
||||
|
|
||||
/// <inheritdoc/>
|
/// <summary>
|
||||
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
/// Gets the <see cref="IBrush"/> used for filling the destination image.
|
||||
{ |
/// </summary>
|
||||
int startX = sourceRectangle.X; |
public IBrush Brush { get; } |
||||
int endX = sourceRectangle.Right; |
|
||||
int startY = sourceRectangle.Y; |
|
||||
int endY = sourceRectangle.Bottom; |
|
||||
|
|
||||
// Align start/end positions.
|
|
||||
int minX = Math.Max(0, startX); |
|
||||
int maxX = Math.Min(source.Width, endX); |
|
||||
int minY = Math.Max(0, startY); |
|
||||
int maxY = Math.Min(source.Height, endY); |
|
||||
|
|
||||
int width = maxX - minX; |
|
||||
|
|
||||
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); |
|
||||
|
|
||||
// If there's no reason for blending, then avoid it.
|
|
||||
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)) |
|
||||
{ |
|
||||
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); |
|
||||
|
|
||||
ParallelHelper.IterateRows( |
|
||||
workingRect, |
|
||||
parallelSettings, |
|
||||
rows => |
|
||||
{ |
|
||||
for (int y = rows.Min; y < rows.Max; y++) |
|
||||
{ |
|
||||
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Reset offset if necessary.
|
|
||||
if (minX > 0) |
|
||||
{ |
|
||||
startX = 0; |
|
||||
} |
|
||||
|
|
||||
if (minY > 0) |
|
||||
{ |
|
||||
startY = 0; |
|
||||
} |
|
||||
|
|
||||
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width)) |
|
||||
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator( |
|
||||
source, |
|
||||
sourceRectangle, |
|
||||
this.options)) |
|
||||
{ |
|
||||
amount.GetSpan().Fill(1f); |
|
||||
|
|
||||
ParallelHelper.IterateRows( |
|
||||
workingRect, |
|
||||
configuration, |
|
||||
rows => |
|
||||
{ |
|
||||
for (int y = rows.Min; y < rows.Max; y++) |
|
||||
{ |
|
||||
int offsetY = y - startY; |
|
||||
int offsetX = minX - startX; |
|
||||
|
|
||||
applicator.Apply(amount.GetSpan(), offsetX, offsetY); |
/// <summary>
|
||||
} |
/// Gets the <see cref="GraphicsOptions"/> defining how to blend the brush pixels over the image pixels.
|
||||
}); |
/// </summary>
|
||||
} |
public GraphicsOptions Options { get; } |
||||
} |
|
||||
} |
|
||||
|
|
||||
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush) |
/// <inheritdoc />
|
||||
|
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>() |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
solidBrush = this.brush as SolidBrush<TPixel>; |
return new FillProcessor<TPixel>(this); |
||||
|
|
||||
if (solidBrush == null) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color); |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.ParallelUtils; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Using the brush as a source of pixels colors blends the brush color with source.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class FillProcessor<TPixel> : ImageProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly FillProcessor definition; |
||||
|
|
||||
|
public FillProcessor(FillProcessor definition) |
||||
|
{ |
||||
|
this.definition = definition; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
int startX = sourceRectangle.X; |
||||
|
int endX = sourceRectangle.Right; |
||||
|
int startY = sourceRectangle.Y; |
||||
|
int endY = sourceRectangle.Bottom; |
||||
|
|
||||
|
// Align start/end positions.
|
||||
|
int minX = Math.Max(0, startX); |
||||
|
int maxX = Math.Min(source.Width, endX); |
||||
|
int minY = Math.Max(0, startY); |
||||
|
int maxY = Math.Min(source.Height, endY); |
||||
|
|
||||
|
int width = maxX - minX; |
||||
|
|
||||
|
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); |
||||
|
|
||||
|
IBrush brush = this.definition.Brush; |
||||
|
GraphicsOptions options = this.definition.Options; |
||||
|
|
||||
|
// If there's no reason for blending, then avoid it.
|
||||
|
if (this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) |
||||
|
{ |
||||
|
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); |
||||
|
|
||||
|
TPixel colorPixel = solidBrush.Color.ToPixel<TPixel>(); |
||||
|
|
||||
|
ParallelHelper.IterateRows( |
||||
|
workingRect, |
||||
|
parallelSettings, |
||||
|
rows => |
||||
|
{ |
||||
|
for (int y = rows.Min; y < rows.Max; y++) |
||||
|
{ |
||||
|
source.GetPixelRowSpan(y).Slice(minX, width).Fill(colorPixel); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Reset offset if necessary.
|
||||
|
if (minX > 0) |
||||
|
{ |
||||
|
startX = 0; |
||||
|
} |
||||
|
|
||||
|
if (minY > 0) |
||||
|
{ |
||||
|
startY = 0; |
||||
|
} |
||||
|
|
||||
|
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width)) |
||||
|
using (BrushApplicator<TPixel> applicator = brush.CreateApplicator( |
||||
|
source, |
||||
|
sourceRectangle, |
||||
|
options)) |
||||
|
{ |
||||
|
amount.GetSpan().Fill(1f); |
||||
|
|
||||
|
ParallelHelper.IterateRows( |
||||
|
workingRect, |
||||
|
configuration, |
||||
|
rows => |
||||
|
{ |
||||
|
for (int y = rows.Min; y < rows.Max; y++) |
||||
|
{ |
||||
|
int offsetY = y - startY; |
||||
|
int offsetX = minX - startX; |
||||
|
|
||||
|
applicator.Apply(amount.GetSpan(), offsetX, offsetY); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) |
||||
|
{ |
||||
|
solidBrush = this.definition.Brush as SolidBrush; |
||||
|
|
||||
|
if (solidBrush == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return this.definition.Options.IsOpaqueColorWithoutBlending(solidBrush.Color); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Primitives; |
||||
|
using SixLabors.ImageSharp.Utils; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Using a brush and a shape fills shape with contents of brush the
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The type of the color.</typeparam>
|
||||
|
/// <seealso cref="ImageProcessor{TPixel}" />
|
||||
|
internal class FillRegionProcessor<TPixel> : ImageProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly FillRegionProcessor definition; |
||||
|
|
||||
|
public FillRegionProcessor(FillRegionProcessor definition) |
||||
|
{ |
||||
|
this.definition = definition; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
GraphicsOptions options = this.definition.Options; |
||||
|
IBrush brush = this.definition.Brush; |
||||
|
Region region = this.definition.Region; |
||||
|
Rectangle rect = region.Bounds; |
||||
|
|
||||
|
// Align start/end positions.
|
||||
|
int minX = Math.Max(0, rect.Left); |
||||
|
int maxX = Math.Min(source.Width, rect.Right); |
||||
|
int minY = Math.Max(0, rect.Top); |
||||
|
int maxY = Math.Min(source.Height, rect.Bottom); |
||||
|
if (minX >= maxX) |
||||
|
{ |
||||
|
return; // no effect inside image;
|
||||
|
} |
||||
|
|
||||
|
if (minY >= maxY) |
||||
|
{ |
||||
|
return; // no effect inside image;
|
||||
|
} |
||||
|
|
||||
|
int maxIntersections = region.MaxIntersections; |
||||
|
float subpixelCount = 4; |
||||
|
|
||||
|
// we need to offset the pixel grid to account for when we outline a path.
|
||||
|
// basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5]
|
||||
|
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the#
|
||||
|
// region to align with the pixel grid.
|
||||
|
float offset = 0.5f; |
||||
|
if (options.Antialias) |
||||
|
{ |
||||
|
offset = 0f; // we are antialiasing skip offsetting as real antialiasing should take care of offset.
|
||||
|
subpixelCount = options.AntialiasSubpixelDepth; |
||||
|
if (subpixelCount < 4) |
||||
|
{ |
||||
|
subpixelCount = 4; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
using (BrushApplicator<TPixel> applicator = brush.CreateApplicator(source, rect, options)) |
||||
|
{ |
||||
|
int scanlineWidth = maxX - minX; |
||||
|
using (IMemoryOwner<float> bBuffer = source.MemoryAllocator.Allocate<float>(maxIntersections)) |
||||
|
using (IMemoryOwner<float> bScanline = source.MemoryAllocator.Allocate<float>(scanlineWidth)) |
||||
|
{ |
||||
|
bool scanlineDirty = true; |
||||
|
float subpixelFraction = 1f / subpixelCount; |
||||
|
float subpixelFractionPoint = subpixelFraction / subpixelCount; |
||||
|
|
||||
|
Span<float> buffer = bBuffer.GetSpan(); |
||||
|
Span<float> scanline = bScanline.GetSpan(); |
||||
|
|
||||
|
bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush); |
||||
|
TPixel solidBrushColor = isSolidBrushWithoutBlending ? solidBrush.Color.ToPixel<TPixel>() : default; |
||||
|
|
||||
|
for (int y = minY; y < maxY; y++) |
||||
|
{ |
||||
|
if (scanlineDirty) |
||||
|
{ |
||||
|
scanline.Clear(); |
||||
|
scanlineDirty = false; |
||||
|
} |
||||
|
|
||||
|
float yPlusOne = y + 1; |
||||
|
for (float subPixel = (float)y; subPixel < yPlusOne; subPixel += subpixelFraction) |
||||
|
{ |
||||
|
int pointsFound = region.Scan(subPixel + offset, buffer, configuration); |
||||
|
if (pointsFound == 0) |
||||
|
{ |
||||
|
// nothing on this line, skip
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
QuickSort.Sort(buffer.Slice(0, pointsFound)); |
||||
|
|
||||
|
for (int point = 0; point < pointsFound; point += 2) |
||||
|
{ |
||||
|
// points will be paired up
|
||||
|
float scanStart = buffer[point] - minX; |
||||
|
float scanEnd = buffer[point + 1] - minX; |
||||
|
int startX = (int)MathF.Floor(scanStart + offset); |
||||
|
int endX = (int)MathF.Floor(scanEnd + offset); |
||||
|
|
||||
|
if (startX >= 0 && startX < scanline.Length) |
||||
|
{ |
||||
|
for (float x = scanStart; x < startX + 1; x += subpixelFraction) |
||||
|
{ |
||||
|
scanline[startX] += subpixelFractionPoint; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (endX >= 0 && endX < scanline.Length) |
||||
|
{ |
||||
|
for (float x = endX; x < scanEnd; x += subpixelFraction) |
||||
|
{ |
||||
|
scanline[endX] += subpixelFractionPoint; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int nextX = startX + 1; |
||||
|
endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge
|
||||
|
nextX = Math.Max(nextX, 0); |
||||
|
for (int x = nextX; x < endX; x++) |
||||
|
{ |
||||
|
scanline[x] += subpixelFraction; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (scanlineDirty) |
||||
|
{ |
||||
|
if (!options.Antialias) |
||||
|
{ |
||||
|
bool hasOnes = false; |
||||
|
bool hasZeros = false; |
||||
|
for (int x = 0; x < scanlineWidth; x++) |
||||
|
{ |
||||
|
if (scanline[x] >= 0.5) |
||||
|
{ |
||||
|
scanline[x] = 1; |
||||
|
hasOnes = true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
scanline[x] = 0; |
||||
|
hasZeros = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isSolidBrushWithoutBlending && hasOnes != hasZeros) |
||||
|
{ |
||||
|
if (hasOnes) |
||||
|
{ |
||||
|
source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrushColor); |
||||
|
} |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
applicator.Apply(scanline, minX, y); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) |
||||
|
{ |
||||
|
solidBrush = this.definition.Brush as SolidBrush; |
||||
|
|
||||
|
if (solidBrush == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return this.definition.Options.IsOpaqueColorWithoutBlending(solidBrush.Color); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,446 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
using SixLabors.Fonts; |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Utils; |
||||
|
using SixLabors.Memory; |
||||
|
using SixLabors.Primitives; |
||||
|
using SixLabors.Shapes; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Text |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Using the brush as a source of pixels colors blends the brush color with source.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class DrawTextProcessor<TPixel> : ImageProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private CachingGlyphRenderer textRenderer; |
||||
|
|
||||
|
private readonly DrawTextProcessor definition; |
||||
|
|
||||
|
public DrawTextProcessor(DrawTextProcessor definition) |
||||
|
{ |
||||
|
this.definition = definition; |
||||
|
} |
||||
|
|
||||
|
private TextGraphicsOptions Options => this.definition.Options; |
||||
|
|
||||
|
private Font Font => this.definition.Font; |
||||
|
|
||||
|
private PointF Location => this.definition.Location; |
||||
|
|
||||
|
private string Text => this.definition.Text; |
||||
|
|
||||
|
private IPen Pen => this.definition.Pen; |
||||
|
|
||||
|
private IBrush Brush => this.definition.Brush; |
||||
|
|
||||
|
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle) |
||||
|
{ |
||||
|
base.BeforeImageApply(source, sourceRectangle); |
||||
|
|
||||
|
// do everything at the image level as we are delegating the processing down to other processors
|
||||
|
var style = new RendererOptions(this.Font, this.Options.DpiX, this.Options.DpiY, this.Location) |
||||
|
{ |
||||
|
ApplyKerning = this.Options.ApplyKerning, |
||||
|
TabWidth = this.Options.TabWidth, |
||||
|
WrappingWidth = this.Options.WrapTextWidth, |
||||
|
HorizontalAlignment = this.Options.HorizontalAlignment, |
||||
|
VerticalAlignment = this.Options.VerticalAlignment |
||||
|
}; |
||||
|
|
||||
|
this.textRenderer = new CachingGlyphRenderer(source.GetMemoryAllocator(), this.Text.Length, this.Pen, this.Brush != null); |
||||
|
this.textRenderer.Options = (GraphicsOptions)this.Options; |
||||
|
var renderer = new TextRenderer(this.textRenderer); |
||||
|
renderer.RenderText(this.Text, style); |
||||
|
} |
||||
|
|
||||
|
protected override void AfterImageApply(Image<TPixel> source, Rectangle sourceRectangle) |
||||
|
{ |
||||
|
base.AfterImageApply(source, sourceRectangle); |
||||
|
this.textRenderer?.Dispose(); |
||||
|
this.textRenderer = null; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
// this is a no-op as we have processes all as an image, we should be able to pass out of before email apply a skip frames outcome
|
||||
|
Draw(this.textRenderer.FillOperations, this.Brush); |
||||
|
Draw(this.textRenderer.OutlineOperations, this.Pen?.StrokeFill); |
||||
|
|
||||
|
void Draw(List<DrawingOperation> operations, IBrush brush) |
||||
|
{ |
||||
|
if (operations?.Count > 0) |
||||
|
{ |
||||
|
using (BrushApplicator<TPixel> app = brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options)) |
||||
|
{ |
||||
|
foreach (DrawingOperation operation in operations) |
||||
|
{ |
||||
|
Buffer2D<float> buffer = operation.Map; |
||||
|
int startY = operation.Location.Y; |
||||
|
int startX = operation.Location.X; |
||||
|
int offSetSpan = 0; |
||||
|
if (startX < 0) |
||||
|
{ |
||||
|
offSetSpan = -startX; |
||||
|
startX = 0; |
||||
|
} |
||||
|
|
||||
|
int fistRow = 0; |
||||
|
if (startY < 0) |
||||
|
{ |
||||
|
fistRow = -startY; |
||||
|
} |
||||
|
|
||||
|
int maxHeight = source.Height - startY; |
||||
|
int end = Math.Min(operation.Map.Height, maxHeight); |
||||
|
|
||||
|
for (int row = fistRow; row < end; row++) |
||||
|
{ |
||||
|
int y = startY + row; |
||||
|
Span<float> span = buffer.GetRowSpan(row).Slice(offSetSpan); |
||||
|
app.Apply(span, startX, y); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private struct DrawingOperation |
||||
|
{ |
||||
|
public Buffer2D<float> Map { get; set; } |
||||
|
|
||||
|
public Point Location { get; set; } |
||||
|
} |
||||
|
|
||||
|
private class CachingGlyphRenderer : IGlyphRenderer, IDisposable |
||||
|
{ |
||||
|
// just enough accuracy to allow for 1/8 pixel differences which
|
||||
|
// later are accumulated while rendering, but do not grow into full pixel offsets
|
||||
|
// The value 8 is benchmarked to:
|
||||
|
// - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant)
|
||||
|
// - Cache hit ratio above 60%
|
||||
|
private const float AccuracyMultiple = 8; |
||||
|
|
||||
|
private readonly PathBuilder builder; |
||||
|
|
||||
|
private Point currentRenderPosition = default; |
||||
|
private (GlyphRendererParameters glyph, PointF subPixelOffset) currentGlyphRenderParams = default; |
||||
|
private readonly int offset = 0; |
||||
|
private PointF currentPoint = default(PointF); |
||||
|
|
||||
|
private readonly Dictionary<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData> |
||||
|
glyphData = new Dictionary<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData>(); |
||||
|
|
||||
|
private readonly bool renderOutline = false; |
||||
|
private readonly bool renderFill = false; |
||||
|
private bool rasterizationRequired = false; |
||||
|
|
||||
|
public CachingGlyphRenderer(MemoryAllocator memoryAllocator, int size, IPen pen, bool renderFill) |
||||
|
{ |
||||
|
this.MemoryAllocator = memoryAllocator; |
||||
|
this.Pen = pen; |
||||
|
this.renderFill = renderFill; |
||||
|
this.renderOutline = pen != null; |
||||
|
this.offset = 2; |
||||
|
if (this.renderFill) |
||||
|
{ |
||||
|
this.FillOperations = new List<DrawingOperation>(size); |
||||
|
} |
||||
|
|
||||
|
if (this.renderOutline) |
||||
|
{ |
||||
|
this.offset = (int)MathF.Ceiling((pen.StrokeWidth * 2) + 2); |
||||
|
this.OutlineOperations = new List<DrawingOperation>(size); |
||||
|
} |
||||
|
|
||||
|
this.builder = new PathBuilder(); |
||||
|
} |
||||
|
|
||||
|
public List<DrawingOperation> FillOperations { get; } |
||||
|
|
||||
|
public List<DrawingOperation> OutlineOperations { get; } |
||||
|
|
||||
|
public MemoryAllocator MemoryAllocator { get; internal set; } |
||||
|
|
||||
|
public IPen Pen { get; internal set; } |
||||
|
|
||||
|
public GraphicsOptions Options { get; internal set; } |
||||
|
|
||||
|
public void BeginFigure() |
||||
|
{ |
||||
|
this.builder.StartFigure(); |
||||
|
} |
||||
|
|
||||
|
public bool BeginGlyph(RectangleF bounds, GlyphRendererParameters parameters) |
||||
|
{ |
||||
|
this.currentRenderPosition = Point.Truncate(bounds.Location); |
||||
|
PointF subPixelOffset = bounds.Location - this.currentRenderPosition; |
||||
|
|
||||
|
subPixelOffset.X = MathF.Round(subPixelOffset.X * AccuracyMultiple) / AccuracyMultiple; |
||||
|
subPixelOffset.Y = MathF.Round(subPixelOffset.Y * AccuracyMultiple) / AccuracyMultiple; |
||||
|
|
||||
|
// we have offset our rendering origion a little bit down to prevent edge cropping, move the draw origin up to compensate
|
||||
|
this.currentRenderPosition = new Point(this.currentRenderPosition.X - this.offset, this.currentRenderPosition.Y - this.offset); |
||||
|
this.currentGlyphRenderParams = (parameters, subPixelOffset); |
||||
|
|
||||
|
if (this.glyphData.ContainsKey(this.currentGlyphRenderParams)) |
||||
|
{ |
||||
|
// we have already drawn the glyph vectors skip trying again
|
||||
|
this.rasterizationRequired = false; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// we check to see if we have a render cache and if we do then we render else
|
||||
|
this.builder.Clear(); |
||||
|
|
||||
|
// ensure all glyphs render around [zero, zero] so offset negative root positions so when we draw the glyph we can offet it back
|
||||
|
this.builder.SetOrigin(new PointF(-(int)bounds.X + this.offset, -(int)bounds.Y + this.offset)); |
||||
|
|
||||
|
this.rasterizationRequired = true; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public void BeginText(RectangleF bounds) |
||||
|
{ |
||||
|
// not concerned about this one
|
||||
|
this.OutlineOperations?.Clear(); |
||||
|
this.FillOperations?.Clear(); |
||||
|
} |
||||
|
|
||||
|
public void CubicBezierTo(PointF secondControlPoint, PointF thirdControlPoint, PointF point) |
||||
|
{ |
||||
|
this.builder.AddBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); |
||||
|
this.currentPoint = point; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
foreach (KeyValuePair<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData> kv in this.glyphData) |
||||
|
{ |
||||
|
kv.Value.Dispose(); |
||||
|
} |
||||
|
|
||||
|
this.glyphData.Clear(); |
||||
|
} |
||||
|
|
||||
|
public void EndFigure() |
||||
|
{ |
||||
|
this.builder.CloseFigure(); |
||||
|
} |
||||
|
|
||||
|
public void EndGlyph() |
||||
|
{ |
||||
|
GlyphRenderData renderData = default; |
||||
|
|
||||
|
// has the glyoh been rendedered already????
|
||||
|
if (this.rasterizationRequired) |
||||
|
{ |
||||
|
IPath path = this.builder.Build(); |
||||
|
|
||||
|
if (this.renderFill) |
||||
|
{ |
||||
|
renderData.FillMap = this.Render(path); |
||||
|
} |
||||
|
|
||||
|
if (this.renderOutline) |
||||
|
{ |
||||
|
if (this.Pen.StrokePattern.Length == 0) |
||||
|
{ |
||||
|
path = path.GenerateOutline(this.Pen.StrokeWidth); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
path = path.GenerateOutline(this.Pen.StrokeWidth, this.Pen.StrokePattern); |
||||
|
} |
||||
|
|
||||
|
renderData.OutlineMap = this.Render(path); |
||||
|
} |
||||
|
|
||||
|
this.glyphData[this.currentGlyphRenderParams] = renderData; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
renderData = this.glyphData[this.currentGlyphRenderParams]; |
||||
|
} |
||||
|
|
||||
|
if (this.renderFill) |
||||
|
{ |
||||
|
this.FillOperations.Add(new DrawingOperation |
||||
|
{ |
||||
|
Location = this.currentRenderPosition, |
||||
|
Map = renderData.FillMap |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (this.renderOutline) |
||||
|
{ |
||||
|
this.OutlineOperations.Add(new DrawingOperation |
||||
|
{ |
||||
|
Location = this.currentRenderPosition, |
||||
|
Map = renderData.OutlineMap |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Buffer2D<float> Render(IPath path) |
||||
|
{ |
||||
|
Size size = Rectangle.Ceiling(path.Bounds).Size; |
||||
|
size = new Size(size.Width + (this.offset * 2), size.Height + (this.offset * 2)); |
||||
|
|
||||
|
float subpixelCount = 4; |
||||
|
float offset = 0.5f; |
||||
|
if (this.Options.Antialias) |
||||
|
{ |
||||
|
offset = 0f; // we are antialising skip offsetting as real antalising should take care of offset.
|
||||
|
subpixelCount = this.Options.AntialiasSubpixelDepth; |
||||
|
if (subpixelCount < 4) |
||||
|
{ |
||||
|
subpixelCount = 4; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// take the path inside the path builder, scan thing and generate a Buffer2d representing the glyph and cache it.
|
||||
|
Buffer2D<float> fullBuffer = this.MemoryAllocator.Allocate2D<float>(size.Width + 1, size.Height + 1, AllocationOptions.Clean); |
||||
|
|
||||
|
using (IMemoryOwner<float> bufferBacking = this.MemoryAllocator.Allocate<float>(path.MaxIntersections)) |
||||
|
using (IMemoryOwner<PointF> rowIntersectionBuffer = this.MemoryAllocator.Allocate<PointF>(size.Width)) |
||||
|
{ |
||||
|
float subpixelFraction = 1f / subpixelCount; |
||||
|
float subpixelFractionPoint = subpixelFraction / subpixelCount; |
||||
|
|
||||
|
for (int y = 0; y <= size.Height; y++) |
||||
|
{ |
||||
|
Span<float> scanline = fullBuffer.GetRowSpan(y); |
||||
|
bool scanlineDirty = false; |
||||
|
float yPlusOne = y + 1; |
||||
|
|
||||
|
for (float subPixel = (float)y; subPixel < yPlusOne; subPixel += subpixelFraction) |
||||
|
{ |
||||
|
var start = new PointF(path.Bounds.Left - 1, subPixel); |
||||
|
var end = new PointF(path.Bounds.Right + 1, subPixel); |
||||
|
Span<PointF> intersectionSpan = rowIntersectionBuffer.GetSpan(); |
||||
|
Span<float> buffer = bufferBacking.GetSpan(); |
||||
|
int pointsFound = path.FindIntersections(start, end, intersectionSpan); |
||||
|
|
||||
|
if (pointsFound == 0) |
||||
|
{ |
||||
|
// nothing on this line skip
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < pointsFound && i < intersectionSpan.Length; i++) |
||||
|
{ |
||||
|
buffer[i] = intersectionSpan[i].X; |
||||
|
} |
||||
|
|
||||
|
QuickSort.Sort(buffer.Slice(0, pointsFound)); |
||||
|
|
||||
|
for (int point = 0; point < pointsFound; point += 2) |
||||
|
{ |
||||
|
// points will be paired up
|
||||
|
float scanStart = buffer[point]; |
||||
|
float scanEnd = buffer[point + 1]; |
||||
|
int startX = (int)MathF.Floor(scanStart + offset); |
||||
|
int endX = (int)MathF.Floor(scanEnd + offset); |
||||
|
|
||||
|
if (startX >= 0 && startX < scanline.Length) |
||||
|
{ |
||||
|
for (float x = scanStart; x < startX + 1; x += subpixelFraction) |
||||
|
{ |
||||
|
scanline[startX] += subpixelFractionPoint; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (endX >= 0 && endX < scanline.Length) |
||||
|
{ |
||||
|
for (float x = endX; x < scanEnd; x += subpixelFraction) |
||||
|
{ |
||||
|
scanline[endX] += subpixelFractionPoint; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int nextX = startX + 1; |
||||
|
endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge
|
||||
|
nextX = Math.Max(nextX, 0); |
||||
|
for (int x = nextX; x < endX; x++) |
||||
|
{ |
||||
|
scanline[x] += subpixelFraction; |
||||
|
scanlineDirty = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (scanlineDirty) |
||||
|
{ |
||||
|
if (!this.Options.Antialias) |
||||
|
{ |
||||
|
for (int x = 0; x < size.Width; x++) |
||||
|
{ |
||||
|
if (scanline[x] >= 0.5) |
||||
|
{ |
||||
|
scanline[x] = 1; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
scanline[x] = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return fullBuffer; |
||||
|
} |
||||
|
|
||||
|
public void EndText() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void LineTo(PointF point) |
||||
|
{ |
||||
|
this.builder.AddLine(this.currentPoint, point); |
||||
|
this.currentPoint = point; |
||||
|
} |
||||
|
|
||||
|
public void MoveTo(PointF point) |
||||
|
{ |
||||
|
this.builder.StartFigure(); |
||||
|
this.currentPoint = point; |
||||
|
} |
||||
|
|
||||
|
public void QuadraticBezierTo(PointF secondControlPoint, PointF point) |
||||
|
{ |
||||
|
this.builder.AddBezier(this.currentPoint, secondControlPoint, point); |
||||
|
this.currentPoint = point; |
||||
|
} |
||||
|
|
||||
|
private struct GlyphRenderData : IDisposable |
||||
|
{ |
||||
|
public Buffer2D<float> FillMap; |
||||
|
|
||||
|
public Buffer2D<float> OutlineMap; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.FillMap?.Dispose(); |
||||
|
this.OutlineMap?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,169 +1,163 @@ |
|||||
// 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 SixLabors.Fonts; |
using SixLabors.Fonts; |
||||
using SixLabors.ImageSharp.PixelFormats; |
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
namespace SixLabors.ImageSharp.Processing |
namespace SixLabors.ImageSharp.Processing |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Options for influencing the drawing functions.
|
/// Options for influencing the drawing functions.
|
||||
/// </summary>
|
/// </summary>
|
||||
public struct TextGraphicsOptions |
public struct TextGraphicsOptions |
||||
{ |
{ |
||||
private const int DefaultTextDpi = 72; |
private const int DefaultTextDpi = 72; |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Represents the default <see cref="TextGraphicsOptions"/>.
|
/// Represents the default <see cref="TextGraphicsOptions"/>.
|
||||
/// </summary>
|
/// </summary>
|
||||
public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); |
public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); |
||||
|
|
||||
private float? blendPercentage; |
private float? blendPercentage; |
||||
|
|
||||
private int? antialiasSubpixelDepth; |
private int? antialiasSubpixelDepth; |
||||
|
|
||||
private bool? antialias; |
private bool? antialias; |
||||
|
|
||||
private bool? applyKerning; |
private bool? applyKerning; |
||||
|
|
||||
private float? tabWidth; |
private float? tabWidth; |
||||
|
|
||||
private float? dpiX; |
private float? dpiX; |
||||
|
|
||||
private float? dpiY; |
private float? dpiY; |
||||
|
|
||||
private PixelColorBlendingMode colorBlendingMode; |
private HorizontalAlignment? horizontalAlignment; |
||||
|
|
||||
private PixelAlphaCompositionMode alphaCompositionMode; |
private VerticalAlignment? verticalAlignment; |
||||
|
|
||||
private float wrapTextWidth; |
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="TextGraphicsOptions" /> struct.
|
||||
private HorizontalAlignment? horizontalAlignment; |
/// </summary>
|
||||
|
/// <param name="enableAntialiasing">If set to <c>true</c> [enable antialiasing].</param>
|
||||
private VerticalAlignment? verticalAlignment; |
public TextGraphicsOptions(bool enableAntialiasing) |
||||
|
{ |
||||
/// <summary>
|
this.applyKerning = true; |
||||
/// Initializes a new instance of the <see cref="TextGraphicsOptions" /> struct.
|
this.tabWidth = 4; |
||||
/// </summary>
|
this.WrapTextWidth = 0; |
||||
/// <param name="enableAntialiasing">If set to <c>true</c> [enable antialiasing].</param>
|
this.horizontalAlignment = HorizontalAlignment.Left; |
||||
public TextGraphicsOptions(bool enableAntialiasing) |
this.verticalAlignment = VerticalAlignment.Top; |
||||
{ |
|
||||
this.applyKerning = true; |
this.antialiasSubpixelDepth = 16; |
||||
this.tabWidth = 4; |
this.ColorBlendingMode = PixelColorBlendingMode.Normal; |
||||
this.wrapTextWidth = 0; |
this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; |
||||
this.horizontalAlignment = HorizontalAlignment.Left; |
this.blendPercentage = 1; |
||||
this.verticalAlignment = VerticalAlignment.Top; |
this.antialias = enableAntialiasing; |
||||
|
this.dpiX = DefaultTextDpi; |
||||
this.antialiasSubpixelDepth = 16; |
this.dpiY = DefaultTextDpi; |
||||
this.colorBlendingMode = PixelColorBlendingMode.Normal; |
} |
||||
this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; |
|
||||
this.blendPercentage = 1; |
/// <summary>
|
||||
this.antialias = enableAntialiasing; |
/// Gets or sets a value indicating whether antialiasing should be applied.
|
||||
this.dpiX = DefaultTextDpi; |
/// </summary>
|
||||
this.dpiY = DefaultTextDpi; |
public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } |
||||
} |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
|
||||
/// Gets or sets a value indicating whether antialiasing should be applied.
|
/// </summary>
|
||||
/// </summary>
|
public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; } |
||||
public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the blending percentage to apply to the drawing operation
|
||||
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
|
/// </summary>
|
||||
/// </summary>
|
public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } |
||||
public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; } |
|
||||
|
// In the future we could expose a PixelBlender<TPixel> directly on here
|
||||
/// <summary>
|
// or some forms of PixelBlender factory for each pixel type. Will need
|
||||
/// Gets or sets a value indicating the blending percentage to apply to the drawing operation
|
// some API thought post V1.
|
||||
/// </summary>
|
|
||||
public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } |
/// <summary>
|
||||
|
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
||||
// In the future we could expose a PixelBlender<TPixel> directly on here
|
/// </summary>
|
||||
// or some forms of PixelBlender factory for each pixel type. Will need
|
public PixelColorBlendingMode ColorBlendingMode { get; set; } |
||||
// some API thought post V1.
|
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
||||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
/// </summary>
|
||||
/// </summary>
|
public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } |
||||
public PixelColorBlendingMode ColorBlendingMode { get => this.colorBlendingMode; set => this.colorBlendingMode = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
|
||||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
/// </summary>
|
||||
/// </summary>
|
public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; } |
||||
public PixelAlphaCompositionMode AlphaCompositionMode { get => this.alphaCompositionMode; set => this.alphaCompositionMode = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the number of space widths a tab should lock to.
|
||||
/// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
|
/// </summary>
|
||||
/// </summary>
|
public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; } |
||||
public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
|
||||
/// Gets or sets a value indicating the number of space widths a tab should lock to.
|
/// </summary>
|
||||
/// </summary>
|
public float WrapTextWidth { get; set; } |
||||
public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the DPI to render text along the X axis.
|
||||
/// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
|
/// </summary>
|
||||
/// </summary>
|
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; } |
||||
public float WrapTextWidth { get => this.wrapTextWidth; set => this.wrapTextWidth = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating the DPI to render text along the Y axis.
|
||||
/// Gets or sets a value indicating the DPI to render text along the X axis.
|
/// </summary>
|
||||
/// </summary>
|
public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; } |
||||
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
||||
/// Gets or sets a value indicating the DPI to render text along the Y axis.
|
/// If <see cref="WrapTextWidth"/> is greater than zero it will align relative to the space
|
||||
/// </summary>
|
/// defined by the location and width, if <see cref="WrapTextWidth"/> equals zero, and thus
|
||||
public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; } |
/// wrapping disabled, then the alignment is relative to the drawing location.
|
||||
|
/// </summary>
|
||||
/// <summary>
|
public HorizontalAlignment HorizontalAlignment { get => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } |
||||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
|
||||
/// If <see cref="WrapTextWidth"/> is greater than zero it will align relative to the space
|
/// <summary>
|
||||
/// defined by the location and width, if <see cref="WrapTextWidth"/> equals zero, and thus
|
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
||||
/// wrapping disabled, then the alignment is relative to the drawing location.
|
/// </summary>
|
||||
/// </summary>
|
public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } |
||||
public HorizontalAlignment HorizontalAlignment { get => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } |
|
||||
|
/// <summary>
|
||||
/// <summary>
|
/// Performs an implicit conversion from <see cref="GraphicsOptions"/> to <see cref="TextGraphicsOptions"/>.
|
||||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
/// </summary>
|
||||
/// </summary>
|
/// <param name="options">The options.</param>
|
||||
public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } |
/// <returns>
|
||||
|
/// The result of the conversion.
|
||||
/// <summary>
|
/// </returns>
|
||||
/// Performs an implicit conversion from <see cref="GraphicsOptions"/> to <see cref="TextGraphicsOptions"/>.
|
public static implicit operator TextGraphicsOptions(GraphicsOptions options) |
||||
/// </summary>
|
{ |
||||
/// <param name="options">The options.</param>
|
return new TextGraphicsOptions(options.Antialias) |
||||
/// <returns>
|
{ |
||||
/// The result of the conversion.
|
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
||||
/// </returns>
|
blendPercentage = options.BlendPercentage, |
||||
public static implicit operator TextGraphicsOptions(GraphicsOptions options) |
ColorBlendingMode = options.ColorBlendingMode, |
||||
{ |
AlphaCompositionMode = options.AlphaCompositionMode |
||||
return new TextGraphicsOptions(options.Antialias) |
}; |
||||
{ |
} |
||||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|
||||
blendPercentage = options.BlendPercentage, |
/// <summary>
|
||||
colorBlendingMode = options.ColorBlendingMode, |
/// Performs an explicit conversion from <see cref="TextGraphicsOptions"/> to <see cref="GraphicsOptions"/>.
|
||||
alphaCompositionMode = options.AlphaCompositionMode |
/// </summary>
|
||||
}; |
/// <param name="options">The options.</param>
|
||||
} |
/// <returns>
|
||||
|
/// The result of the conversion.
|
||||
/// <summary>
|
/// </returns>
|
||||
/// Performs an explicit conversion from <see cref="TextGraphicsOptions"/> to <see cref="GraphicsOptions"/>.
|
public static explicit operator GraphicsOptions(TextGraphicsOptions options) |
||||
/// </summary>
|
{ |
||||
/// <param name="options">The options.</param>
|
return new GraphicsOptions(options.Antialias) |
||||
/// <returns>
|
{ |
||||
/// The result of the conversion.
|
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
||||
/// </returns>
|
|
||||
public static explicit operator GraphicsOptions(TextGraphicsOptions options) |
|
||||
{ |
|
||||
return new GraphicsOptions(options.Antialias) |
|
||||
{ |
|
||||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|
||||
ColorBlendingMode = options.ColorBlendingMode, |
ColorBlendingMode = options.ColorBlendingMode, |
||||
AlphaCompositionMode = options.AlphaCompositionMode, |
AlphaCompositionMode = options.AlphaCompositionMode, |
||||
BlendPercentage = options.BlendPercentage |
BlendPercentage = options.BlendPercentage |
||||
}; |
}; |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -1,92 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
|
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class Beziers : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByBezierLine() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "BezierLine"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Color.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawBeziers( |
|
||||
Rgba32.HotPink, |
|
||||
5, |
|
||||
new SixLabors.Primitives.PointF[] |
|
||||
{ |
|
||||
new Vector2(10, 400), new Vector2(30, 10), new Vector2(240, 30), new Vector2(300, 400) |
|
||||
})); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
//top of curve
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[138, 115]); |
|
||||
|
|
||||
//start points
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 395]); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[300, 395]); |
|
||||
|
|
||||
//curve points should not be never be set
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[30, 10]); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[240, 30]); |
|
||||
|
|
||||
// inside shape should be empty
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[200, 250]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedBezierLineWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "BezierLine"); |
|
||||
|
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Color.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawBeziers( |
|
||||
color, |
|
||||
10, |
|
||||
new SixLabors.Primitives.PointF[] |
|
||||
{ |
|
||||
new Vector2(10, 400), new Vector2(30, 10), new Vector2(240, 30), new Vector2(300, 400) |
|
||||
})); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards foreground color by the opacity amount
|
|
||||
var mergedColor = new Rgba32( |
|
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
// top of curve
|
|
||||
Assert.Equal(mergedColor, sourcePixels[138, 115]); |
|
||||
|
|
||||
// start points
|
|
||||
Assert.Equal(mergedColor, sourcePixels[10, 395]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[300, 395]); |
|
||||
|
|
||||
// curve points should not be never be set
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[30, 10]); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[240, 30]); |
|
||||
|
|
||||
// inside shape should be empty
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[200, 250]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,48 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class DrawBezierTests |
||||
|
{ |
||||
|
public static readonly TheoryData<string, byte, float> DrawPathData = new TheoryData<string, byte, float> |
||||
|
{ |
||||
|
{ "White", 255, 1.5f }, |
||||
|
{ "Red", 255, 3 }, |
||||
|
{ "HotPink", 255, 5 }, |
||||
|
{ "HotPink", 150, 5 }, |
||||
|
{ "White", 255, 15 }, |
||||
|
}; |
||||
|
|
||||
|
[Theory] |
||||
|
[WithSolidFilledImages(nameof(DrawPathData), 300, 450, "Blue", PixelTypes.Rgba32)] |
||||
|
public void DrawBeziers<TPixel>(TestImageProvider<TPixel> provider, string colorName, byte alpha, float thickness) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var points = new SixLabors.Primitives.PointF[] |
||||
|
{ |
||||
|
new Vector2(10, 400), new Vector2(30, 10), new Vector2(240, 30), new Vector2(300, 400) |
||||
|
}; |
||||
|
Rgba32 rgba = TestUtils.GetColorByName(colorName); |
||||
|
rgba.A = alpha; |
||||
|
Color color = rgba; |
||||
|
|
||||
|
FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( x => x.DrawBeziers(color, 5f, points), |
||||
|
testDetails, |
||||
|
appendSourceFileOrDescription: false, |
||||
|
appendPixelTypeToFileName: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Shapes; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class DrawComplexPolygonTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, true, false, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, true, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, true)] |
||||
|
public void DrawComplexPolygon<TPixel>(TestImageProvider<TPixel> provider, bool overlap, bool transparent, bool dashed) |
||||
|
where TPixel :struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var simplePath = new Polygon(new LinearLineSegment( |
||||
|
new Vector2(10, 10), |
||||
|
new Vector2(200, 150), |
||||
|
new Vector2(50, 300))); |
||||
|
|
||||
|
var hole1 = new Polygon(new LinearLineSegment( |
||||
|
new Vector2(37, 85), |
||||
|
overlap ? new Vector2(130, 40) : new Vector2(93, 85), |
||||
|
new Vector2(65, 137))); |
||||
|
IPath clipped = simplePath.Clip(hole1); |
||||
|
|
||||
|
Rgba32 colorRgba = Rgba32.White; |
||||
|
if (transparent) |
||||
|
{ |
||||
|
colorRgba.A = 150; |
||||
|
} |
||||
|
|
||||
|
Color color = colorRgba; |
||||
|
|
||||
|
string testDetails = ""; |
||||
|
if (overlap) |
||||
|
{ |
||||
|
testDetails += "_Overlap"; |
||||
|
} |
||||
|
|
||||
|
if (transparent) |
||||
|
{ |
||||
|
testDetails += "_Transparent"; |
||||
|
} |
||||
|
|
||||
|
if (dashed) |
||||
|
{ |
||||
|
testDetails += "_Dashed"; |
||||
|
} |
||||
|
|
||||
|
Pen pen = dashed ? Pens.Dash(color, 5f) : Pens.Solid(color, 5f); |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
x => x.Draw(pen, clipped), |
||||
|
testDetails, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class DrawLinesTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] |
||||
|
public void DrawLines_Simple<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
Pen pen = new Pen(color, thickness); |
||||
|
|
||||
|
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] |
||||
|
public void DrawLines_Dash<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
Pen pen = Pens.Dash(color, thickness); |
||||
|
|
||||
|
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1f, 5, false)] |
||||
|
public void DrawLines_Dot<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
Pen pen = Pens.Dot(color, thickness); |
||||
|
|
||||
|
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, false)] |
||||
|
public void DrawLines_DashDot<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
Pen pen = Pens.DashDot(color, thickness); |
||||
|
|
||||
|
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Black", 1f, 5, false)] |
||||
|
public void DrawLines_DashDotDot<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
Pen pen = Pens.DashDotDot(color, thickness); |
||||
|
|
||||
|
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static void DrawLinesImpl<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
string colorName, |
||||
|
float alpha, |
||||
|
float thickness, |
||||
|
bool antialias, |
||||
|
Pen pen) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
SixLabors.Primitives.PointF[] simplePath = { new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) }; |
||||
|
|
||||
|
GraphicsOptions options = new GraphicsOptions(antialias); |
||||
|
|
||||
|
string aa = antialias ? "" : "_NoAntialias"; |
||||
|
FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.DrawLines(options, pen, simplePath), |
||||
|
outputDetails, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -1,112 +1,79 @@ |
|||||
// 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; |
||||
using System.Numerics; |
using System.Numerics; |
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
using SixLabors.ImageSharp.Memory; |
||||
using SixLabors.ImageSharp.PixelFormats; |
using SixLabors.ImageSharp.PixelFormats; |
||||
using SixLabors.ImageSharp.Processing; |
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Primitives; |
||||
using SixLabors.Shapes; |
using SixLabors.Shapes; |
||||
using Xunit; |
using Xunit; |
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
{ |
{ |
||||
public class DrawPathTests : FileTestBase |
[GroupOutput("Drawing")] |
||||
|
public class DrawPathTests |
||||
{ |
{ |
||||
[Fact] |
public static readonly TheoryData<string, byte, float> DrawPathData = new TheoryData<string, byte, float> |
||||
public void ImageShouldBeOverlayedByPath() |
{ |
||||
|
{ "White", 255, 1.5f }, |
||||
|
{ "Red", 255, 3 }, |
||||
|
{ "HotPink", 255, 5 }, |
||||
|
{ "HotPink", 150, 5 }, |
||||
|
{ "White", 255, 15 }, |
||||
|
}; |
||||
|
|
||||
|
[Theory] |
||||
|
[WithSolidFilledImages(nameof(DrawPathData), 300, 450, "Blue", PixelTypes.Rgba32)] |
||||
|
public void DrawPath<TPixel>(TestImageProvider<TPixel> provider, string colorName, byte alpha, float thickness) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Path"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
var linerSegemnt = new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300)); |
|
||||
var bazierSegment = new CubicBezierLineSegment( |
|
||||
new Vector2(50, 300), |
|
||||
new Vector2(500, 500), |
|
||||
new Vector2(60, 10), |
|
||||
new Vector2(10, 400)); |
|
||||
|
|
||||
var p = new Path(linerSegemnt, bazierSegment); |
|
||||
|
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(Rgba32.HotPink, 5, p)); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPathWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Path"); |
|
||||
|
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
|
|
||||
var linerSegemnt = new LinearLineSegment( |
var linerSegemnt = new LinearLineSegment( |
||||
new Vector2(10, 10), |
new Vector2(10, 10), |
||||
new Vector2(200, 150), |
new Vector2(200, 150), |
||||
new Vector2(50, 300) |
new Vector2(50, 300)); |
||||
); |
var bazierSegment = new CubicBezierLineSegment( |
||||
|
new Vector2(50, 300), |
||||
var bazierSegment = new CubicBezierLineSegment(new Vector2(50, 300), |
|
||||
new Vector2(500, 500), |
new Vector2(500, 500), |
||||
new Vector2(60, 10), |
new Vector2(60, 10), |
||||
new Vector2(10, 400)); |
new Vector2(10, 400)); |
||||
|
|
||||
var p = new Path(linerSegemnt, bazierSegment); |
var path = new Path(linerSegemnt, bazierSegment); |
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(color, 10, p)); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
Rgba32 rgba = TestUtils.GetColorByName(colorName); |
||||
var mergedColor = new Rgba32( |
rgba.A = alpha; |
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
Color color = rgba; |
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; |
||||
Assert.Equal(mergedColor, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(mergedColor, sourcePixels[199, 149]); |
provider.RunValidatingProcessorTest( |
||||
|
x => x.Draw(color, thickness, path), |
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
testDetails, |
||||
} |
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
} |
} |
||||
|
|
||||
[Fact] |
[Theory] |
||||
public void PathExtendingOffEdgeOfImageShouldNotBeCropped() |
[WithSolidFilledImages(256, 256, "Black", PixelTypes.Rgba32)] |
||||
|
public void PathExtendingOffEdgeOfImageShouldNotBeCropped<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
|
Color color = Color.White; |
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Path"); |
Pen pen = Pens.Solid(color, 5f); |
||||
using (var image = new Image<Rgba32>(256, 256)) |
|
||||
{ |
provider.RunValidatingProcessorTest( |
||||
image.Mutate(x => x.Fill(Rgba32.Black)); |
x => |
||||
Pen<Rgba32> pen = Pens.Solid(Rgba32.White, 5f); |
{ |
||||
|
for (int i = 0; i < 300; i += 20) |
||||
for (int i = 0; i < 300; i += 20) |
{ |
||||
{ |
PointF[] points = new PointF[] { new Vector2(100, 2), new Vector2(-10, i) }; |
||||
image.Mutate( |
x.DrawLines(pen, points); |
||||
x => x.DrawLines( |
} |
||||
pen, |
}, |
||||
new SixLabors.Primitives.PointF[] { new Vector2(100, 2), new Vector2(-10, i) })); |
appendPixelTypeToFileName: false, |
||||
} |
appendSourceFileOrDescription: false); |
||||
|
|
||||
image.Save($"{path}/ClippedLines.png"); |
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.White, sourcePixels[0, 90]); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class DrawPolygonTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] |
||||
|
public void DrawPolygon<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, float thickness, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
SixLabors.Primitives.PointF[] simplePath = |
||||
|
{ |
||||
|
new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) |
||||
|
}; |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
|
||||
|
GraphicsOptions options = new GraphicsOptions(antialias); |
||||
|
|
||||
|
string aa = antialias ? "" : "_NoAntialias"; |
||||
|
FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.DrawPolygon(options, color, thickness, simplePath), |
||||
|
outputDetails, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Shapes; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class FillComplexPolygonTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, false)] |
||||
|
[WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, true, false)] |
||||
|
[WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, true)] |
||||
|
public void ComplexPolygon_SolidFill<TPixel>(TestImageProvider<TPixel> provider, bool overlap, bool transparent) |
||||
|
where TPixel :struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var simplePath = new Polygon(new LinearLineSegment( |
||||
|
new Vector2(10, 10), |
||||
|
new Vector2(200, 150), |
||||
|
new Vector2(50, 300))); |
||||
|
|
||||
|
var hole1 = new Polygon(new LinearLineSegment( |
||||
|
new Vector2(37, 85), |
||||
|
overlap ? new Vector2(130, 40) : new Vector2(93, 85), |
||||
|
new Vector2(65, 137))); |
||||
|
IPath clipped = simplePath.Clip(hole1); |
||||
|
|
||||
|
Rgba32 colorRgba = Rgba32.HotPink; |
||||
|
if (transparent) |
||||
|
{ |
||||
|
colorRgba.A = 150; |
||||
|
} |
||||
|
|
||||
|
Color color = colorRgba; |
||||
|
|
||||
|
string testDetails = ""; |
||||
|
if (overlap) |
||||
|
{ |
||||
|
testDetails += "_Overlap"; |
||||
|
} |
||||
|
|
||||
|
if (transparent) |
||||
|
{ |
||||
|
testDetails += "_Transparent"; |
||||
|
} |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
x => x.Fill(color, clipped), |
||||
|
testDetails, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,155 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Shapes; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class FillPolygonTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, true)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, false)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, true)] |
||||
|
public void FillPolygon_Solid<TPixel>(TestImageProvider<TPixel> provider, string colorName, float alpha, bool antialias) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
SixLabors.Primitives.PointF[] simplePath = |
||||
|
{ |
||||
|
new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) |
||||
|
}; |
||||
|
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); |
||||
|
|
||||
|
GraphicsOptions options = new GraphicsOptions(antialias); |
||||
|
|
||||
|
string aa = antialias ? "" : "_NoAntialias"; |
||||
|
FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.FillPolygon(options, color, simplePath), |
||||
|
outputDetails, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32)] |
||||
|
public void FillPolygon_Concave<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var points = new SixLabors.Primitives.PointF[] |
||||
|
{ |
||||
|
new Vector2(8, 8), |
||||
|
new Vector2(64, 8), |
||||
|
new Vector2(64, 64), |
||||
|
new Vector2(120, 64), |
||||
|
new Vector2(120, 120), |
||||
|
new Vector2(8, 120) |
||||
|
}; |
||||
|
|
||||
|
Color color = Color.LightGreen; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.FillPolygon(color, points), |
||||
|
appendSourceFileOrDescription: false, |
||||
|
appendPixelTypeToFileName: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] |
||||
|
public void FillPolygon_Pattern<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
SixLabors.Primitives.PointF[] simplePath = |
||||
|
{ |
||||
|
new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) |
||||
|
}; |
||||
|
Color color = Color.Yellow; |
||||
|
|
||||
|
var brush = Brushes.Horizontal(color); |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.FillPolygon(brush, simplePath), |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)] |
||||
|
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)] |
||||
|
public void FillPolygon_ImageBrush<TPixel>(TestImageProvider<TPixel> provider, string brushImageName) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
SixLabors.Primitives.PointF[] simplePath = |
||||
|
{ |
||||
|
new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) |
||||
|
}; |
||||
|
|
||||
|
using (Image<TPixel> brushImage = Image.Load<TPixel>(TestFile.Create(brushImageName).Bytes)) |
||||
|
{ |
||||
|
var brush = new ImageBrush(brushImage); |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.FillPolygon(brush, simplePath), |
||||
|
System.IO.Path.GetFileNameWithoutExtension(brushImageName), |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(250, 250, PixelTypes.Rgba32)] |
||||
|
public void Fill_RectangularPolygon<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var polygon = new SixLabors.Shapes.RectangularPolygon(10, 10, 190, 140); |
||||
|
Color color = Color.White; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.Fill(color, polygon), |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 50, 0f)] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, 20f)] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, -180f)] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 5, 70, 0f)] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 7, 80, -180f)] |
||||
|
public void Fill_RegularPolygon<TPixel>(TestImageProvider<TPixel> provider, int vertices, float radius, float angleDeg) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
float angle = GeometryUtilities.DegreeToRadian(angleDeg); |
||||
|
var polygon = new RegularPolygon(100, 100, vertices, radius, angle); |
||||
|
Color color = Color.Yellow; |
||||
|
|
||||
|
FormattableString testOutput = $"V({vertices})_R({radius})_Ang({angleDeg})"; |
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.Fill(color, polygon), |
||||
|
testOutput, |
||||
|
appendSourceFileOrDescription: false, |
||||
|
appendPixelTypeToFileName: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32)] |
||||
|
public void Fill_EllipsePolygon<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var polygon = new EllipsePolygon(100, 100, 80, 120); |
||||
|
Color color = Color.Azure; |
||||
|
|
||||
|
provider.RunValidatingProcessorTest( |
||||
|
c => c.Fill(color, polygon), |
||||
|
appendSourceFileOrDescription: false, |
||||
|
appendPixelTypeToFileName: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,201 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using SixLabors.Shapes; |
|
||||
|
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class LineComplexPolygonTests : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutline() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "LineComplexPolygon"); |
|
||||
|
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(93, 85), |
|
||||
new Vector2(65, 137))); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(Rgba32.HotPink, 5, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 10]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[200, 150]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[50, 300]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[37, 85]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[93, 85]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[65, 137]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[57, 99]); |
|
||||
|
|
||||
//inside shape
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[100, 192]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutlineNoOverlapping() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "LineComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(207, 25), |
|
||||
new Vector2(263, 25), |
|
||||
new Vector2(235, 57))); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(Rgba32.HotPink, 5, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/SimpleVanishHole.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 10]); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[200, 150]); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[50, 300]); |
|
||||
|
|
||||
//Assert.Equal(Color.HotPink, sourcePixels[37, 85]);
|
|
||||
|
|
||||
//Assert.Equal(Color.HotPink, sourcePixels[93, 85]);
|
|
||||
|
|
||||
//Assert.Equal(Color.HotPink, sourcePixels[65, 137]);
|
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[57, 99]); |
|
||||
|
|
||||
//inside shape
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[100, 192]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutlineOverlapping() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "LineComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(130, 40), |
|
||||
new Vector2(65, 137))); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(Rgba32.HotPink, 5, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/SimpleOverlapping.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 10]); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[200, 150]); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[50, 300]); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[130, 41]); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[57, 99]); |
|
||||
|
|
||||
//inside shape
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[100, 192]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutlineDashed() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "LineComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(93, 85), |
|
||||
new Vector2(65, 137))); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(Pens.Dash(Rgba32.HotPink, 5), simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/Dashed.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "LineComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(93, 85), |
|
||||
new Vector2(65, 137))); |
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Draw(color, 5, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
|
||||
var mergedColor = new Rgba32( |
|
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(mergedColor, sourcePixels[10, 10]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[200, 150]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[50, 300]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[37, 85]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[93, 85]); |
|
||||
Assert.Equal(mergedColor, sourcePixels[65, 137]); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[57, 99]); |
|
||||
|
|
||||
//inside shape
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[100, 192]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,193 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
|
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class LineTests : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPath() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawLines( |
|
||||
Rgba32.HotPink, |
|
||||
5, |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPath_NoAntialias() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawLines( |
|
||||
new GraphicsOptions(false), |
|
||||
Rgba32.HotPink, |
|
||||
5, |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
image.Save($"{path}/Simple_noantialias.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPathDashed() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.DrawLines(Pens.Dash(Rgba32.HotPink, 5), |
|
||||
new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
})); |
|
||||
image.Save($"{path}/Dashed.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPathDotted() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.DrawLines(Pens.Dot(Rgba32.HotPink, 5), |
|
||||
new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
})); |
|
||||
image.Save($"{path}/Dot.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPathDashDot() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.DrawLines(Pens.DashDot(Rgba32.HotPink, 5), |
|
||||
new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
})); |
|
||||
image.Save($"{path}/DashDot.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPathDashDotDot() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
var image = new Image<Rgba32>(500, 500); |
|
||||
|
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.DrawLines(Pens.DashDotDot(Rgba32.HotPink, 5), |
|
||||
new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
})); |
|
||||
image.Save($"{path}/DashDotDot.png"); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPathWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
|
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
var image = new Image<Rgba32>(500, 500); |
|
||||
|
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawLines( |
|
||||
color, |
|
||||
10, |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
|
||||
var mergedColor = |
|
||||
new Rgba32(Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(mergedColor, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(mergedColor, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPathOutline() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Lines"); |
|
||||
|
|
||||
var image = new Image<Rgba32>(500, 500); |
|
||||
|
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawLines( |
|
||||
Rgba32.HotPink, |
|
||||
10, |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(10, 150))); |
|
||||
image.Save($"{path}/Rectangle.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[198, 10]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[10, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,102 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.Primitives; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class PolygonTests : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutline() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Polygons"); |
|
||||
|
|
||||
using (Image<Rgba32> image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.DrawPolygon( |
|
||||
Rgba32.HotPink, |
|
||||
5, |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Polygons"); |
|
||||
PointF[] simplePath = { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
|
|
||||
Rgba32 color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
using (Image<Rgba32> image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.DrawPolygon(color, 10, simplePath)); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
|
||||
Rgba32 mergedColor = new Rgba32( |
|
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(mergedColor, sourcePixels[9, 9]); |
|
||||
|
|
||||
Assert.Equal(mergedColor, sourcePixels[199, 149]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByRectangleOutline() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "Polygons"); |
|
||||
|
|
||||
using (Image<Rgba32> image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.Draw(Rgba32.HotPink, 10, new Rectangle(10, 10, 190, 140))); |
|
||||
image.Save($"{path}/Rectangle.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[8, 8]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[198, 10]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[50, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,48 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using SixLabors.Primitives; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests |
|
||||
{ |
|
||||
public class RecolorImageTest : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldRecolorYellowToHotPink() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "RecolorImage"); |
|
||||
|
|
||||
var brush = new RecolorBrush<Rgba32>(Rgba32.Yellow, Rgba32.HotPink, 0.2f); |
|
||||
|
|
||||
foreach (TestFile file in Files) |
|
||||
{ |
|
||||
using (Image<Rgba32> image = file.CreateRgba32Image()) |
|
||||
{ |
|
||||
image.Mutate(x => x.Fill(brush)); |
|
||||
image.Save($"{path}/{file.FileName}"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldRecolorYellowToHotPinkInARectangle() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "RecolorImage"); |
|
||||
|
|
||||
var brush = new RecolorBrush<Rgba32>(Rgba32.Yellow, Rgba32.HotPink, 0.2f); |
|
||||
|
|
||||
foreach (TestFile file in Files) |
|
||||
{ |
|
||||
using (Image<Rgba32> image = file.CreateRgba32Image()) |
|
||||
{ |
|
||||
int imageHeight = image.Height; |
|
||||
image.Mutate(x => x.Fill(brush, new Rectangle(0, imageHeight / 2 - imageHeight / 4, image.Width, imageHeight / 2))); |
|
||||
image.Save($"{path}/Shaped_{file.FileName}"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,52 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
[GroupOutput("Drawing")] |
||||
|
public class RecolorImageTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, "Yellow", "Pink", 0.2f)] |
||||
|
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] |
||||
|
[WithTestPatternImages(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] |
||||
|
[WithTestPatternImages(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.6f)] |
||||
|
public void Recolor<TPixel>(TestImageProvider<TPixel> provider, string sourceColorName, string targetColorName, float threshold) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color sourceColor = TestUtils.GetColorByName(sourceColorName); |
||||
|
Color targetColor = TestUtils.GetColorByName(targetColorName); |
||||
|
var brush = new RecolorBrush(sourceColor, targetColor, threshold); |
||||
|
|
||||
|
FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; |
||||
|
provider.RunValidatingProcessorTest(x => x.Fill(brush), testInfo); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] |
||||
|
[WithTestPatternImages(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] |
||||
|
public void Recolor_InBox<TPixel>(TestImageProvider<TPixel> provider, string sourceColorName, string targetColorName, float threshold) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Color sourceColor = TestUtils.GetColorByName(sourceColorName); |
||||
|
Color targetColor = TestUtils.GetColorByName(targetColorName); |
||||
|
var brush = new RecolorBrush(sourceColor, targetColor, threshold); |
||||
|
|
||||
|
FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; |
||||
|
provider.RunValidatingProcessorTest(x => |
||||
|
{ |
||||
|
Size size = x.GetCurrentSize(); |
||||
|
var rectangle = new Rectangle(0, size.Height / 2 - size.Height / 4, size.Width, size.Height / 2); |
||||
|
x.Fill(brush, rectangle); |
||||
|
}, testInfo); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,109 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using SixLabors.Shapes; |
|
||||
|
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class SolidComplexPolygonTests : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByPolygonOutline() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "ComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(93, 85), |
|
||||
new Vector2(65, 137))); |
|
||||
IPath clipped = simplePath.Clip(hole1); |
|
||||
// var clipped = new Rectangle(10, 10, 100, 100).Clip(new Rectangle(20, 0, 20, 20));
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Fill(Rgba32.HotPink, clipped)); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[20, 35]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[60, 100]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPolygonOutlineWithOverlap() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "ComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(130, 40), |
|
||||
new Vector2(65, 137))); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Fill(Rgba32.HotPink, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/SimpleOverlapping.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[20, 35]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[60, 100]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "ComplexPolygon"); |
|
||||
var simplePath = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300))); |
|
||||
|
|
||||
var hole1 = new Polygon(new LinearLineSegment( |
|
||||
new Vector2(37, 85), |
|
||||
new Vector2(93, 85), |
|
||||
new Vector2(65, 137))); |
|
||||
|
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Fill(color, simplePath.Clip(hole1))); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
|
||||
var mergedColor = new Rgba32( |
|
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(mergedColor, sourcePixels[20, 35]); |
|
||||
|
|
||||
//inside hole
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[60, 100]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,240 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Numerics; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using SixLabors.Shapes; |
|
||||
|
|
||||
using Xunit; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
|
||||
{ |
|
||||
public class SolidPolygonTests : FileTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledPolygon() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
SixLabors.Primitives.PointF[] simplePath = { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.FillPolygon(new GraphicsOptions(true), Rgba32.HotPink, simplePath)); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[81, 145]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledPolygonWithPattern() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
var simplePath = new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate( |
|
||||
x => x.FillPolygon(new GraphicsOptions(true), Brushes.Horizontal(Rgba32.HotPink), simplePath)); |
|
||||
image.Save($"{path}/Pattern.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[81, 145]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledPolygonNoAntialias() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
var simplePath = new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.FillPolygon( |
|
||||
new GraphicsOptions(false), |
|
||||
Rgba32.HotPink, |
|
||||
simplePath)); |
|
||||
image.Save($"{path}/Simple_NoAntialias.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.True(Rgba32.HotPink == sourcePixels[11, 11], "[11, 11] wrong"); |
|
||||
|
|
||||
Assert.True(Rgba32.HotPink == sourcePixels[199, 149], "[199, 149] wrong"); |
|
||||
|
|
||||
Assert.True(Rgba32.HotPink == sourcePixels[50, 50], "[50, 50] wrong"); |
|
||||
|
|
||||
Assert.True(Rgba32.Blue == sourcePixels[2, 2], "[2, 2] wrong"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledPolygonImage() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
var simplePath = new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
|
|
||||
using (Image<Rgba32> brushImage = TestFile.Create(TestImages.Bmp.Car).CreateRgba32Image()) |
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
var brush = new ImageBrush<Rgba32>(brushImage); |
|
||||
|
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.FillPolygon(brush, simplePath)); |
|
||||
image.Save($"{path}/Image.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledPolygonOpacity() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
var simplePath = new SixLabors.Primitives.PointF[] { |
|
||||
new Vector2(10, 10), |
|
||||
new Vector2(200, 150), |
|
||||
new Vector2(50, 300) |
|
||||
}; |
|
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.FillPolygon(color, simplePath)); |
|
||||
image.Save($"{path}/Opacity.png"); |
|
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
|
||||
var mergedColor = new Rgba32( |
|
||||
Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledRectangle() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.Fill( |
|
||||
Rgba32.HotPink, |
|
||||
new SixLabors.Shapes.RectangularPolygon(10, 10, 190, 140))); |
|
||||
image.Save($"{path}/Rectangle.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[11, 11]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[198, 10]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[10, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[50, 50]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[2, 2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledTriangle() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
|
|
||||
using (var image = new Image<Rgba32>(100, 100)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate( |
|
||||
x => x.Fill(Rgba32.HotPink, new RegularPolygon(50, 50, 3, 30))); |
|
||||
image.Save($"{path}/Triangle.png"); |
|
||||
|
|
||||
Buffer2D<Rgba32> sourcePixels = image.GetRootFramePixelBuffer(); |
|
||||
Assert.Equal(Rgba32.Blue, sourcePixels[30, 65]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[50, 50]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledSeptagon() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
|
|
||||
var config = Configuration.CreateDefaultInstance(); |
|
||||
config.MaxDegreeOfParallelism = 1; |
|
||||
using (var image = new Image<Rgba32>(config, 100, 100)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x.Fill(Rgba32.HotPink, |
|
||||
new RegularPolygon(50, 50, 7, 30, -(float)Math.PI))); |
|
||||
image.Save($"{path}/Septagon.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedByFilledEllipse() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
|
|
||||
var config = Configuration.CreateDefaultInstance(); |
|
||||
config.MaxDegreeOfParallelism = 1; |
|
||||
using (var image = new Image<Rgba32>(config, 100, 100)) |
|
||||
{ |
|
||||
image.Mutate(x => x.BackgroundColor(Rgba32.Blue)); |
|
||||
image.Mutate(x => x |
|
||||
.Fill(Rgba32.HotPink, new EllipsePolygon(50, 50, 30, 50) |
|
||||
.Rotate((float)(Math.PI / 3)))); |
|
||||
image.Save($"{path}/ellipse.png"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void ImageShouldBeOverlayedBySquareWithCornerClipped() |
|
||||
{ |
|
||||
string path = TestEnvironment.CreateOutputDirectory("Drawing", "FilledPolygons"); |
|
||||
|
|
||||
var config = Configuration.CreateDefaultInstance(); |
|
||||
config.MaxDegreeOfParallelism = 1; |
|
||||
using (var image = new Image<Rgba32>(config, 200, 200)) |
|
||||
{ |
|
||||
image.Mutate(x => x |
|
||||
.Fill(Rgba32.Blue) |
|
||||
.FillPolygon(Rgba32.HotPink, new SixLabors.Primitives.PointF[] |
|
||||
{ |
|
||||
new Vector2( 8, 8 ), |
|
||||
new Vector2( 64, 8 ), |
|
||||
new Vector2( 64, 64 ), |
|
||||
new Vector2( 120, 64 ), |
|
||||
new Vector2( 120, 120 ), |
|
||||
new Vector2( 8, 120 ) |
|
||||
})); |
|
||||
image.Save($"{path}/clipped-corner.png"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue