diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
index fdeb27a77..722ef33ff 100644
--- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
+++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
@@ -40,9 +40,9 @@
+
-
diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs
index 0c6e0d3b4..7e75d7eff 100644
--- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs
+++ b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs
@@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing
///
/// Gets the blend percentage
///
- protected GraphicsOptions Options { get; private set; }
+ protected GraphicsOptions Options { get; }
///
/// Gets the color for a single pixel.
diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs
index 21690055e..a7a6785b9 100644
--- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs
@@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing
}
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The brush.
internal PatternBrush(PatternBrush brush)
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs
index 0fbef9afe..54b7315b2 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs
@@ -1,20 +1,29 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
-using System;
using System.Buffers;
using System.Threading.Tasks;
-using SixLabors.ImageSharp.Advanced;
+
using SixLabors.ImageSharp.Memory;
-using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
+ ///
+ /// Combines two images together by blending the pixels.
+ ///
public class DrawImageProcessor : IImageProcessor
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The image to blend.
+ /// The location to draw the blended image.
+ /// The blending mode to use when drawing the image.
+ /// The Alpha blending mode to use when drawing the image.
+ /// The opacity of the image to blend.
public DrawImageProcessor(
Image image,
Point location,
@@ -30,27 +39,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
}
///
- /// Gets the image to blend
+ /// Gets the image to blend.
///
public Image Image { get; }
///
- /// Gets the location to draw the blended image
+ /// Gets the location to draw the blended image.
///
public Point Location { get; }
-
+
///
/// Gets the blending mode to use when drawing the image.
///
public PixelColorBlendingMode ColorBlendingMode { get; }
-
+
///
/// Gets the Alpha blending mode to use when drawing the image.
///
public PixelAlphaCompositionMode AlphaCompositionMode { get; }
-
+
///
- /// Gets the opacity of the image to blend
+ /// Gets the opacity of the image to blend.
///
public float Opacity { get; }
@@ -74,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
}
public IImageProcessor Result { get; private set; }
-
+
public void Visit(Image image)
where TPixelFg : struct, IPixel
{
@@ -83,108 +92,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
this.definition.Location,
this.definition.ColorBlendingMode,
this.definition.AlphaCompositionMode,
- this.definition.Opacity
- );
+ this.definition.Opacity);
}
}
}
-
- ///
- /// Combines two images together by blending the pixels.
- ///
- /// The pixel format of destination image.
- /// The pixel format of source image.
- internal class DrawImageProcessor : ImageProcessor
- where TPixelBg : struct, IPixel
- where TPixelFg : struct, IPixel
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The image to blend with the currently processing image.
- /// The location to draw the blended image.
- /// The blending mode to use when drawing the image.
- /// The Alpha blending mode to use when drawing the image.
- /// The opacity of the image to blend. Must be between 0 and 1.
- public DrawImageProcessor(
- Image 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.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
- this.Location = location;
- }
-
- ///
- /// Gets the image to blend
- ///
- public Image Image { get; }
-
- ///
- /// Gets the opacity of the image to blend
- ///
- public float Opacity { get; }
-
- ///
- /// Gets the pixel blender
- ///
- public PixelBlender Blender { get; }
-
- ///
- /// Gets the location to draw the blended image
- ///
- public Point Location { get; }
-
- ///
- protected override void OnFrameApply(
- ImageFrame source,
- Rectangle sourceRectangle,
- Configuration configuration)
- {
- Image targetImage = this.Image;
- PixelBlender 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 background = source.GetPixelRowSpan(y).Slice(minX, width);
- Span foreground =
- targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
- blender.Blend(configuration, background, background, foreground, this.Opacity);
- }
- });
- }
- }
}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
new file mode 100644
index 000000000..21599bf78
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
@@ -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
+{
+ ///
+ /// Combines two images together by blending the pixels.
+ ///
+ /// The pixel format of destination image.
+ /// The pixel format of source image.
+ internal class DrawImageProcessor : ImageProcessor
+ where TPixelBg : struct, IPixel
+ where TPixelFg : struct, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The image to blend with the currently processing image.
+ /// The location to draw the blended image.
+ /// The blending mode to use when drawing the image.
+ /// The Alpha blending mode to use when drawing the image.
+ /// The opacity of the image to blend. Must be between 0 and 1.
+ public DrawImageProcessor(
+ Image 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.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
+ this.Location = location;
+ }
+
+ ///
+ /// Gets the image to blend
+ ///
+ public Image Image { get; }
+
+ ///
+ /// Gets the opacity of the image to blend
+ ///
+ public float Opacity { get; }
+
+ ///
+ /// Gets the pixel blender
+ ///
+ public PixelBlender Blender { get; }
+
+ ///
+ /// Gets the location to draw the blended image
+ ///
+ public Point Location { get; }
+
+ ///
+ protected override void OnFrameApply(
+ ImageFrame source,
+ Rectangle sourceRectangle,
+ Configuration configuration)
+ {
+ Image targetImage = this.Image;
+ PixelBlender 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 background = source.GetPixelRowSpan(y).Slice(minX, width);
+ Span foreground =
+ targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
+ blender.Blend(configuration, background, background, foreground, this.Opacity);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs
index 8ea1c48cc..d6254c7cf 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs
@@ -1,28 +1,38 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
-using System;
-using System.Buffers;
using System.Threading.Tasks;
-using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Memory;
-using SixLabors.ImageSharp.ParallelUtils;
+
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
-using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
+ ///
+ /// Defines a processor to fill an with the given
+ /// using blending defined by the given .
+ ///
public class FillProcessor : IImageProcessor
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The brush to use for filling.
+ /// The defining how to blend the brush pixels over the image pixels.
public FillProcessor(IBrush brush, GraphicsOptions options)
{
this.Brush = brush;
this.Options = options;
}
+ ///
+ /// Gets the used for filling the destination image.
+ ///
public IBrush Brush { get; }
-
+
+ ///
+ /// Gets the defining how to blend the brush pixels over the image pixels.
+ ///
public GraphicsOptions Options { get; }
///
@@ -32,108 +42,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
return new FillProcessor(this);
}
}
-
- ///
- /// Using the brush as a source of pixels colors blends the brush color with source.
- ///
- /// The pixel format.
- internal class FillProcessor : ImageProcessor
- where TPixel : struct, IPixel
- {
- private readonly FillProcessor definition;
-
- public FillProcessor(FillProcessor definition)
- {
- this.definition = definition;
- }
-
- ///
- protected override void OnFrameApply(ImageFrame 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();
-
- 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 amount = source.MemoryAllocator.Allocate(width))
- using (BrushApplicator 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);
- }
- }
}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs
new file mode 100644
index 000000000..68aef82e2
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs
@@ -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
+{
+ ///
+ /// Using the brush as a source of pixels colors blends the brush color with source.
+ ///
+ /// The pixel format.
+ internal class FillProcessor : ImageProcessor
+ where TPixel : struct, IPixel
+ {
+ private readonly FillProcessor definition;
+
+ public FillProcessor(FillProcessor definition)
+ {
+ this.definition = definition;
+ }
+
+ ///
+ protected override void OnFrameApply(ImageFrame 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();
+
+ 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 amount = source.MemoryAllocator.Allocate(width))
+ using (BrushApplicator 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
index 3653102ef..1b207d4cd 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
@@ -1,18 +1,16 @@
// 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.Memory;
-using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
+ ///
+ /// Defines a processor to fill pixels withing a given
+ /// with the given and blending defined by the given .
+ ///
public class FillRegionProcessor : IImageProcessor
{
///
@@ -29,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
}
///
- /// Gets the brush.
+ /// Gets the used for filling the destination image.
///
public IBrush Brush { get; }
@@ -39,11 +37,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
public Region Region { get; }
///
- /// Gets the options.
+ /// Gets the defining how to blend the brush pixels over the image pixels.
///
- ///
- /// The options.
- ///
public GraphicsOptions Options { get; }
///
@@ -53,184 +48,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
return new FillRegionProcessor(this);
}
}
-
- ///
- /// Using a brush and a shape fills shape with contents of brush the
- ///
- /// The type of the color.
- ///
- internal class FillRegionProcessor : ImageProcessor
- where TPixel : struct, IPixel
- {
- private readonly FillRegionProcessor definition;
-
- public FillRegionProcessor(FillRegionProcessor definition)
- {
- this.definition = definition;
- }
-
- ///
- protected override void OnFrameApply(ImageFrame 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 applicator = brush.CreateApplicator(source, rect, options))
- {
- int scanlineWidth = maxX - minX;
- using (IMemoryOwner bBuffer = source.MemoryAllocator.Allocate(maxIntersections))
- using (IMemoryOwner bScanline = source.MemoryAllocator.Allocate(scanlineWidth))
- {
- bool scanlineDirty = true;
- float subpixelFraction = 1f / subpixelCount;
- float subpixelFractionPoint = subpixelFraction / subpixelCount;
-
- Span buffer = bBuffer.GetSpan();
- Span scanline = bScanline.GetSpan();
-
- bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush);
- TPixel solidBrushColor = isSolidBrushWithoutBlending ? solidBrush.Color.ToPixel() : 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);
- }
- }
}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs
new file mode 100644
index 000000000..d5778c865
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs
@@ -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
+{
+ ///
+ /// Using a brush and a shape fills shape with contents of brush the
+ ///
+ /// The type of the color.
+ ///
+ internal class FillRegionProcessor : ImageProcessor
+ where TPixel : struct, IPixel
+ {
+ private readonly FillRegionProcessor definition;
+
+ public FillRegionProcessor(FillRegionProcessor definition)
+ {
+ this.definition = definition;
+ }
+
+ ///
+ protected override void OnFrameApply(ImageFrame 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 applicator = brush.CreateApplicator(source, rect, options))
+ {
+ int scanlineWidth = maxX - minX;
+ using (IMemoryOwner bBuffer = source.MemoryAllocator.Allocate(maxIntersections))
+ using (IMemoryOwner bScanline = source.MemoryAllocator.Allocate(scanlineWidth))
+ {
+ bool scanlineDirty = true;
+ float subpixelFraction = 1f / subpixelCount;
+ float subpixelFractionPoint = subpixelFraction / subpixelCount;
+
+ Span buffer = bBuffer.GetSpan();
+ Span scanline = bScanline.GetSpan();
+
+ bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush);
+ TPixel solidBrushColor = isSolidBrushWithoutBlending ? solidBrush.Color.ToPixel() : 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs
index d1e28afeb..40621ce99 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs
@@ -2,19 +2,16 @@
// 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
{
+ ///
+ /// Defines a processor to draw text on an .
+ ///
public class DrawTextProcessor : IImageProcessor
{
///
@@ -45,17 +42,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text
}
///
- /// Gets the brush.
+ /// Gets the brush used to fill the glyphs.
///
public IBrush Brush { get; }
///
- /// Gets the options
+ /// Gets the defining blending modes and text-specific drawing settings.
///
public TextGraphicsOptions Options { get; }
///
- /// Gets the text
+ /// Gets the text to draw.
///
public string Text { get; }
@@ -81,432 +78,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text
return new DrawTextProcessor(this);
}
}
-
- ///
- /// Using the brush as a source of pixels colors blends the brush color with source.
- ///
- /// The pixel format.
- internal class DrawTextProcessor : ImageProcessor
- where TPixel : struct, IPixel
- {
- 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 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 source, Rectangle sourceRectangle)
- {
- base.AfterImageApply(source, sourceRectangle);
- this.textRenderer?.Dispose();
- this.textRenderer = null;
- }
-
- ///
- protected override void OnFrameApply(ImageFrame 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 operations, IBrush brush)
- {
- if (operations?.Count > 0)
- {
- using (BrushApplicator app = brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options))
- {
- foreach (DrawingOperation operation in operations)
- {
- Buffer2D 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 span = buffer.GetRowSpan(row).Slice(offSetSpan);
- app.Apply(span, startX, y);
- }
- }
- }
- }
- }
- }
-
- private struct DrawingOperation
- {
- public Buffer2D 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(size);
- }
-
- if (this.renderOutline)
- {
- this.offset = (int)MathF.Ceiling((pen.StrokeWidth * 2) + 2);
- this.OutlineOperations = new List(size);
- }
-
- this.builder = new PathBuilder();
- }
-
- public List FillOperations { get; }
-
- public List 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 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 fullBuffer = this.MemoryAllocator.Allocate2D(size.Width + 1, size.Height + 1, AllocationOptions.Clean);
-
- using (IMemoryOwner bufferBacking = this.MemoryAllocator.Allocate(path.MaxIntersections))
- using (IMemoryOwner rowIntersectionBuffer = this.MemoryAllocator.Allocate(size.Width))
- {
- float subpixelFraction = 1f / subpixelCount;
- float subpixelFractionPoint = subpixelFraction / subpixelCount;
-
- for (int y = 0; y <= size.Height; y++)
- {
- Span 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 intersectionSpan = rowIntersectionBuffer.GetSpan();
- Span 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 FillMap;
-
- public Buffer2D OutlineMap;
-
- public void Dispose()
- {
- this.FillMap?.Dispose();
- this.OutlineMap?.Dispose();
- }
- }
- }
- }
}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs
new file mode 100644
index 000000000..9fb52d6bc
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs
@@ -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
+{
+ ///
+ /// Using the brush as a source of pixels colors blends the brush color with source.
+ ///
+ /// The pixel format.
+ internal class DrawTextProcessor : ImageProcessor
+ where TPixel : struct, IPixel
+ {
+ 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 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 source, Rectangle sourceRectangle)
+ {
+ base.AfterImageApply(source, sourceRectangle);
+ this.textRenderer?.Dispose();
+ this.textRenderer = null;
+ }
+
+ ///
+ protected override void OnFrameApply(ImageFrame 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 operations, IBrush brush)
+ {
+ if (operations?.Count > 0)
+ {
+ using (BrushApplicator app = brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options))
+ {
+ foreach (DrawingOperation operation in operations)
+ {
+ Buffer2D 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 span = buffer.GetRowSpan(row).Slice(offSetSpan);
+ app.Apply(span, startX, y);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private struct DrawingOperation
+ {
+ public Buffer2D 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(size);
+ }
+
+ if (this.renderOutline)
+ {
+ this.offset = (int)MathF.Ceiling((pen.StrokeWidth * 2) + 2);
+ this.OutlineOperations = new List(size);
+ }
+
+ this.builder = new PathBuilder();
+ }
+
+ public List FillOperations { get; }
+
+ public List 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 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 fullBuffer = this.MemoryAllocator.Allocate2D(size.Width + 1, size.Height + 1, AllocationOptions.Clean);
+
+ using (IMemoryOwner bufferBacking = this.MemoryAllocator.Allocate(path.MaxIntersections))
+ using (IMemoryOwner rowIntersectionBuffer = this.MemoryAllocator.Allocate(size.Width))
+ {
+ float subpixelFraction = 1f / subpixelCount;
+ float subpixelFractionPoint = subpixelFraction / subpixelCount;
+
+ for (int y = 0; y <= size.Height; y++)
+ {
+ Span 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 intersectionSpan = rowIntersectionBuffer.GetSpan();
+ Span 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 FillMap;
+
+ public Buffer2D OutlineMap;
+
+ public void Dispose()
+ {
+ this.FillMap?.Dispose();
+ this.OutlineMap?.Dispose();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs
index 84a97e7d1..aac0f2e7b 100644
--- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs
@@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing
/// A Circular Gradient Brush, defined by center point and radius.
///
public sealed class RadialGradientBrush : GradientBrushBase
-
{
private readonly PointF center;
diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs
index 75ca5aa7e..c62566f6b 100644
--- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs
@@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing
private readonly Color color;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The color.
public SolidBrush(Color color)
@@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Processing
where TPixel : struct, IPixel
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The source image.
/// The color.
diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
index 3c682a761..7f7332a57 100644
--- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
@@ -1,169 +1,163 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the Apache License, Version 2.0.
-
-using SixLabors.Fonts;
-using SixLabors.ImageSharp.PixelFormats;
-
-namespace SixLabors.ImageSharp.Processing
-{
- ///
- /// Options for influencing the drawing functions.
- ///
- public struct TextGraphicsOptions
- {
- private const int DefaultTextDpi = 72;
-
- ///
- /// Represents the default .
- ///
- public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true);
-
- private float? blendPercentage;
-
- private int? antialiasSubpixelDepth;
-
- private bool? antialias;
-
- private bool? applyKerning;
-
- private float? tabWidth;
-
- private float? dpiX;
-
- private float? dpiY;
-
- private PixelColorBlendingMode colorBlendingMode;
-
- private PixelAlphaCompositionMode alphaCompositionMode;
-
- private float wrapTextWidth;
-
- private HorizontalAlignment? horizontalAlignment;
-
- private VerticalAlignment? verticalAlignment;
-
- ///
- /// Initializes a new instance of the struct.
- ///
- /// If set to true [enable antialiasing].
- public TextGraphicsOptions(bool enableAntialiasing)
- {
- this.applyKerning = true;
- this.tabWidth = 4;
- this.wrapTextWidth = 0;
- this.horizontalAlignment = HorizontalAlignment.Left;
- this.verticalAlignment = VerticalAlignment.Top;
-
- this.antialiasSubpixelDepth = 16;
- this.colorBlendingMode = PixelColorBlendingMode.Normal;
- this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver;
- this.blendPercentage = 1;
- this.antialias = enableAntialiasing;
- this.dpiX = DefaultTextDpi;
- this.dpiY = DefaultTextDpi;
- }
-
- ///
- /// Gets or sets a value indicating whether antialiasing should be applied.
- ///
- public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; }
-
- ///
- /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
- ///
- public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; }
-
- ///
- /// Gets or sets a value indicating the blending percentage to apply to the drawing operation
- ///
- public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; }
-
- // In the future we could expose a PixelBlender directly on here
- // or some forms of PixelBlender factory for each pixel type. Will need
- // some API thought post V1.
-
- ///
- /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
- ///
- public PixelColorBlendingMode ColorBlendingMode { get => this.colorBlendingMode; set => this.colorBlendingMode = value; }
-
- ///
- /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
- ///
- public PixelAlphaCompositionMode AlphaCompositionMode { get => this.alphaCompositionMode; set => this.alphaCompositionMode = value; }
-
- ///
- /// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
- ///
- public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; }
-
- ///
- /// Gets or sets a value indicating the number of space widths a tab should lock to.
- ///
- public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; }
-
- ///
- /// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
- ///
- public float WrapTextWidth { get => this.wrapTextWidth; set => this.wrapTextWidth = value; }
-
- ///
- /// Gets or sets a value indicating the DPI to render text along the X axis.
- ///
- public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; }
-
- ///
- /// Gets or sets a value indicating the DPI to render text along the Y axis.
- ///
- public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; }
-
- ///
- /// Gets or sets a value indicating how to align the text relative to the rendering space.
- /// If is greater than zero it will align relative to the space
- /// defined by the location and width, if equals zero, and thus
- /// wrapping disabled, then the alignment is relative to the drawing location.
- ///
- 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.
- ///
- public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; }
-
- ///
- /// Performs an implicit conversion from to .
- ///
- /// The options.
- ///
- /// The result of the conversion.
- ///
- public static implicit operator TextGraphicsOptions(GraphicsOptions options)
- {
- return new TextGraphicsOptions(options.Antialias)
- {
- AntialiasSubpixelDepth = options.AntialiasSubpixelDepth,
- blendPercentage = options.BlendPercentage,
- colorBlendingMode = options.ColorBlendingMode,
- alphaCompositionMode = options.AlphaCompositionMode
- };
- }
-
- ///
- /// Performs an explicit conversion from to .
- ///
- /// The options.
- ///
- /// The result of the conversion.
- ///
- public static explicit operator GraphicsOptions(TextGraphicsOptions options)
- {
- return new GraphicsOptions(options.Antialias)
- {
- AntialiasSubpixelDepth = options.AntialiasSubpixelDepth,
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.Fonts;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Processing
+{
+ ///
+ /// Options for influencing the drawing functions.
+ ///
+ public struct TextGraphicsOptions
+ {
+ private const int DefaultTextDpi = 72;
+
+ ///
+ /// Represents the default .
+ ///
+ public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true);
+
+ private float? blendPercentage;
+
+ private int? antialiasSubpixelDepth;
+
+ private bool? antialias;
+
+ private bool? applyKerning;
+
+ private float? tabWidth;
+
+ private float? dpiX;
+
+ private float? dpiY;
+
+ private HorizontalAlignment? horizontalAlignment;
+
+ private VerticalAlignment? verticalAlignment;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// If set to true [enable antialiasing].
+ public TextGraphicsOptions(bool enableAntialiasing)
+ {
+ this.applyKerning = true;
+ this.tabWidth = 4;
+ this.WrapTextWidth = 0;
+ this.horizontalAlignment = HorizontalAlignment.Left;
+ this.verticalAlignment = VerticalAlignment.Top;
+
+ this.antialiasSubpixelDepth = 16;
+ this.ColorBlendingMode = PixelColorBlendingMode.Normal;
+ this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver;
+ this.blendPercentage = 1;
+ this.antialias = enableAntialiasing;
+ this.dpiX = DefaultTextDpi;
+ this.dpiY = DefaultTextDpi;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether antialiasing should be applied.
+ ///
+ public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; }
+
+ ///
+ /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
+ ///
+ public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; }
+
+ ///
+ /// Gets or sets a value indicating the blending percentage to apply to the drawing operation
+ ///
+ public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; }
+
+ // In the future we could expose a PixelBlender directly on here
+ // or some forms of PixelBlender factory for each pixel type. Will need
+ // some API thought post V1.
+
+ ///
+ /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
+ ///
+ public PixelColorBlendingMode ColorBlendingMode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
+ ///
+ public PixelAlphaCompositionMode AlphaCompositionMode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
+ ///
+ public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; }
+
+ ///
+ /// Gets or sets a value indicating the number of space widths a tab should lock to.
+ ///
+ public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; }
+
+ ///
+ /// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
+ ///
+ public float WrapTextWidth { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the DPI to render text along the X axis.
+ ///
+ public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; }
+
+ ///
+ /// Gets or sets a value indicating the DPI to render text along the Y axis.
+ ///
+ public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; }
+
+ ///
+ /// Gets or sets a value indicating how to align the text relative to the rendering space.
+ /// If is greater than zero it will align relative to the space
+ /// defined by the location and width, if equals zero, and thus
+ /// wrapping disabled, then the alignment is relative to the drawing location.
+ ///
+ 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.
+ ///
+ public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; }
+
+ ///
+ /// Performs an implicit conversion from to .
+ ///
+ /// The options.
+ ///
+ /// The result of the conversion.
+ ///
+ public static implicit operator TextGraphicsOptions(GraphicsOptions options)
+ {
+ return new TextGraphicsOptions(options.Antialias)
+ {
+ AntialiasSubpixelDepth = options.AntialiasSubpixelDepth,
+ blendPercentage = options.BlendPercentage,
+ ColorBlendingMode = options.ColorBlendingMode,
+ AlphaCompositionMode = options.AlphaCompositionMode
+ };
+ }
+
+ ///
+ /// Performs an explicit conversion from to .
+ ///
+ /// The options.
+ ///
+ /// The result of the conversion.
+ ///
+ public static explicit operator GraphicsOptions(TextGraphicsOptions options)
+ {
+ return new GraphicsOptions(options.Antialias)
+ {
+ AntialiasSubpixelDepth = options.AntialiasSubpixelDepth,
ColorBlendingMode = options.ColorBlendingMode,
- AlphaCompositionMode = options.AlphaCompositionMode,
- BlendPercentage = options.BlendPercentage
- };
- }
- }
+ AlphaCompositionMode = options.AlphaCompositionMode,
+ BlendPercentage = options.BlendPercentage
+ };
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs
index db0328106..3f3d6a62f 100644
--- a/src/ImageSharp/Image.cs
+++ b/src/ImageSharp/Image.cs
@@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp
EncodeVisitor visitor = new EncodeVisitor(encoder, stream);
this.AcceptVisitor(visitor);
}
-
+
///
/// Returns a copy of the image in the given pixel format.
///
diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj
index 4abe2f57a..1b213c26d 100644
--- a/src/ImageSharp/ImageSharp.csproj
+++ b/src/ImageSharp/ImageSharp.csproj
@@ -41,7 +41,7 @@
-
+