Browse Source

add optermised drawing path with cached glyph rendering

af/merge-core
Scott Williams 8 years ago
parent
commit
9cc4fd683d
  1. 19
      src/ImageSharp.Drawing/Processing/Drawing/Processors/FillRegionProcessor.cs
  2. 30
      src/ImageSharp.Drawing/Processing/Text/DrawTextExtensions.Path.cs
  3. 32
      src/ImageSharp.Drawing/Processing/Text/DrawTextExtensions.cs
  4. 140
      src/ImageSharp.Drawing/Processing/Text/Processors/DrawTextOnPathProcessor.cs
  5. 458
      src/ImageSharp.Drawing/Processing/Text/Processors/DrawTextProcessor.cs
  6. 19
      src/ImageSharp.Drawing/Processing/Text/TextGraphicsOptions.cs
  7. 101
      tests/ImageSharp.Benchmarks/Drawing/DrawText.cs
  8. 138
      tests/ImageSharp.Benchmarks/Drawing/OldProcessors/DrawTextProcessorV1.cs
  9. 61
      tests/ImageSharp.Tests/Drawing/Text/DrawText.Path.cs
  10. 101
      tests/ImageSharp.Tests/Drawing/Text/DrawText.cs
  11. 22
      tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs
  12. 6
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj

19
src/ImageSharp.Drawing/Processing/Drawing/Processors/FillRegionProcessor.cs

@ -37,22 +37,29 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Processors
}
/// <summary>
/// Gets the brush.
/// Initializes a new instance of the <see cref="FillRegionProcessor{TPixel}" /> class.
/// </summary>
public FillRegionProcessor()
{
}
/// <summary>
/// Gets or sets the brush.
/// </summary>
public IBrush<TPixel> Brush { get; }
public IBrush<TPixel> Brush { get; set; }
/// <summary>
/// Gets the region that this processor applies to.
/// Gets or sets the region that this processor applies to.
/// </summary>
public Region Region { get; }
public Region Region { get; set; }
/// <summary>
/// Gets the options.
/// Gets or sets the options.
/// </summary>
/// <value>
/// The options.
/// </value>
public GraphicsOptions Options { get; }
public GraphicsOptions Options { get; set; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)

30
src/ImageSharp.Drawing/Processing/Text/DrawTextExtensions.Path.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Drawing;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Text.Processors;
using SixLabors.Shapes;
namespace SixLabors.ImageSharp.Processing.Text
@ -147,33 +148,6 @@ namespace SixLabors.ImageSharp.Processing.Text
/// </returns>
public static IImageProcessingContext<TPixel> DrawText<TPixel>(this IImageProcessingContext<TPixel> source, TextGraphicsOptions options, string text, Font font, IBrush<TPixel> brush, IPen<TPixel> pen, IPath path)
where TPixel : struct, IPixel<TPixel>
{
float dpiX = DefaultTextDpi;
float dpiY = DefaultTextDpi;
var style = new RendererOptions(font, dpiX, dpiY)
{
ApplyKerning = options.ApplyKerning,
TabWidth = options.TabWidth,
WrappingWidth = options.WrapTextWidth,
HorizontalAlignment = options.HorizontalAlignment,
VerticalAlignment = options.VerticalAlignment
};
IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, path, style);
var pathOptions = (GraphicsOptions)options;
if (brush != null)
{
source.Fill(pathOptions, brush, glyphs);
}
if (pen != null)
{
source.Draw(pathOptions, pen, glyphs);
}
return source;
}
=> source.ApplyProcessor(new DrawTextOnPathProcessor<TPixel>(options, text, font, brush, pen, path));
}
}

32
src/ImageSharp.Drawing/Processing/Text/DrawTextExtensions.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Drawing;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Text.Processors;
using SixLabors.Primitives;
using SixLabors.Shapes;
@ -16,8 +17,6 @@ namespace SixLabors.ImageSharp.Processing.Text
/// </summary>
public static partial class DrawTextExtensions
{
private static readonly int DefaultTextDpi = 72;
/// <summary>
/// Draws the text onto the the image filled via the brush.
/// </summary>
@ -150,33 +149,6 @@ namespace SixLabors.ImageSharp.Processing.Text
/// </returns>
public static IImageProcessingContext<TPixel> DrawText<TPixel>(this IImageProcessingContext<TPixel> source, TextGraphicsOptions options, string text, Font font, IBrush<TPixel> brush, IPen<TPixel> pen, PointF location)
where TPixel : struct, IPixel<TPixel>
{
float dpiX = DefaultTextDpi;
float dpiY = DefaultTextDpi;
var style = new RendererOptions(font, dpiX, dpiY, location)
{
ApplyKerning = options.ApplyKerning,
TabWidth = options.TabWidth,
WrappingWidth = options.WrapTextWidth,
HorizontalAlignment = options.HorizontalAlignment,
VerticalAlignment = options.VerticalAlignment
};
IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, style);
var pathOptions = (GraphicsOptions)options;
if (brush != null)
{
source.Fill(pathOptions, brush, glyphs);
}
if (pen != null)
{
source.Draw(pathOptions, pen, glyphs);
}
return source;
}
=> source.ApplyProcessor(new DrawTextProcessor<TPixel>(options, text, font, brush, pen, location));
}
}

140
src/ImageSharp.Drawing/Processing/Text/Processors/DrawTextOnPathProcessor.cs

@ -0,0 +1,140 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading.Tasks;
using SixLabors.Fonts;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Drawing.Processors;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
using SixLabors.Shapes;
namespace SixLabors.ImageSharp.Processing.Text.Processors
{
/// <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 DrawTextOnPathProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private FillRegionProcessor<TPixel> fillRegionProcessor = null;
/// <summary>
/// Initializes a new instance of the <see cref="DrawTextOnPathProcessor{TPixel}"/> class.
/// </summary>
/// <param name="options">The options</param>
/// <param name="text">The text we want to render</param>
/// <param name="font">The font we want to render with</param>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="pen">The pen to outline text with.</param>
/// <param name="path">The path on which to draw the text along.</param>
public DrawTextOnPathProcessor(TextGraphicsOptions options, string text, Font font, IBrush<TPixel> brush, IPen<TPixel> pen, IPath path)
{
this.Brush = brush;
this.Options = options;
this.Text = text;
this.Pen = pen;
this.Font = font;
this.Path = path;
}
/// <summary>
/// Gets or sets the brush.
/// </summary>
public IBrush<TPixel> Brush { get; set; }
/// <summary>
/// Gets or sets the options
/// </summary>
private TextGraphicsOptions Options { get; set; }
/// <summary>
/// Gets or sets the text
/// </summary>
private string Text { get; set; }
/// <summary>
/// Gets or sets the pen used for outlining the text, if Null then we will not outline
/// </summary>
public IPen<TPixel> Pen { get; set; }
/// <summary>
/// Gets or sets the font used to render the text.
/// </summary>
public Font Font { get; set; }
/// <summary>
/// Gets or sets the path to draw the text along.
/// </summary>
public IPath Path { get; set; }
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle)
{
base.BeforeImageApply(source, sourceRectangle);
// do everythign at the image level as we are deligating the processing down to other processors
var style = new RendererOptions(this.Font, this.Options.DpiX, this.Options.DpiY)
{
ApplyKerning = this.Options.ApplyKerning,
TabWidth = this.Options.TabWidth,
WrappingWidth = this.Options.WrapTextWidth,
HorizontalAlignment = this.Options.HorizontalAlignment,
VerticalAlignment = this.Options.VerticalAlignment
};
IPathCollection glyphs = TextBuilder.GenerateGlyphs(this.Text, this.Path, style);
var pathOptions = (GraphicsOptions)this.Options;
if (this.Brush != null)
{
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapeRegion(p);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
if (this.Pen != null)
{
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapePath(p, this.Pen);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
}
/// <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
}
}
}

458
src/ImageSharp.Drawing/Processing/Text/Processors/DrawTextProcessor.cs

@ -0,0 +1,458 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using SixLabors.Fonts;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Drawing.Processors;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
using SixLabors.Shapes;
namespace SixLabors.ImageSharp.Processing.Text.Processors
{
/// <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 FillRegionProcessor<TPixel> fillRegionProcessor = null;
private CachingGlyphRenderer textRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="DrawTextProcessor{TPixel}"/> class.
/// </summary>
/// <param name="options">The options</param>
/// <param name="text">The text we want to render</param>
/// <param name="font">The font we want to render with</param>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="pen">The pen to outline text with.</param>
/// <param name="location">The location on the image to start drawign the text from.</param>
public DrawTextProcessor(TextGraphicsOptions options, string text, Font font, IBrush<TPixel> brush, IPen<TPixel> pen, PointF location)
{
this.Brush = brush;
this.Options = options;
this.Text = text;
this.Pen = pen;
this.Font = font;
this.Location = location;
}
/// <summary>
/// Gets or sets the brush.
/// </summary>
public IBrush<TPixel> Brush { get; set; }
/// <summary>
/// Gets or sets the options
/// </summary>
public TextGraphicsOptions Options { get; set; }
/// <summary>
/// Gets or sets the text
/// </summary>
public string Text { get; set; }
/// <summary>
/// Gets or sets the pen used for outlining the text, if Null then we will not outline
/// </summary>
public IPen<TPixel> Pen { get; set; }
/// <summary>
/// Gets or sets the font used to render the text.
/// </summary>
public Font Font { get; set; }
/// <summary>
/// Gets or sets the location to draw the text at.
/// </summary>
public PointF Location { get; set; }
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle)
{
base.BeforeImageApply(source, sourceRectangle);
// user slow path if pen is set and fast render for brush only rendering
// do everythign at the image level as we are deligating 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
};
if (this.Pen != null)
{
IPathCollection glyphs = TextBuilder.GenerateGlyphs(this.Text, style);
var pathOptions = (GraphicsOptions)this.Options;
if (this.Brush != null)
{
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapeRegion(p);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapePath(p, this.Pen);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
else
{
this.textRenderer = new CachingGlyphRenderer(source.GetMemoryManager());
this.textRenderer.Options = (GraphicsOptions)this.Options;
TextRenderer.RenderTextTo(this.textRenderer, 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
if (this.Pen == null && this.Brush != null && this.textRenderer != null && this.textRenderer.Operations.Count > 0)
{
// we have rendered at the image level now we can draw
List<DrawingOperation> operations = this.textRenderer.Operations;
using (BrushApplicator<TPixel> app = this.Brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options))
{
foreach (DrawingOperation operation in operations)
{
IBuffer2D<float> buffer = operation.Map;
int startY = operation.Location.Y;
int startX = operation.Location.X;
int end = operation.Map.Height;
for (int row = 0; row < end; row++)
{
int y = startY + row;
app.Apply(buffer.GetRowSpan(row), startX, y);
}
}
}
}
}
private struct DrawingOperation
{
public IBuffer2D<float> Map { get; set; }
public Point Location { get; set; }
}
private class CachingGlyphRenderer : IGlyphRenderer, IDisposable
{
private PathBuilder builder;
private Point currentRenderPosition = default(Point);
private int currentRenderingGlyph = 0;
private PointF currentPoint = default(PointF);
private Dictionary<int, Buffer2D<float>> glyphMap = new Dictionary<int, Buffer2D<float>>();
public CachingGlyphRenderer(MemoryManager memoryManager)
{
this.MemoryManager = memoryManager;
this.builder = new PathBuilder();
}
public List<DrawingOperation> Operations { get; } = new List<DrawingOperation>();
public MemoryManager MemoryManager { get; internal set; }
public GraphicsOptions Options { get; internal set; }
public void BeginFigure()
{
this.builder.StartFigure();
}
public bool BeginGlyph(RectangleF bounds, int cacheKey)
{
this.currentRenderPosition = Point.Truncate(bounds.Location);
this.currentRenderingGlyph = cacheKey;
if (this.glyphMap.ContainsKey(this.currentRenderingGlyph))
{
// we have already drawn the glyph vectors skip trying again
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, -(int)bounds.Y));
return true;
}
public void BeginText(RectangleF bounds)
{
// not concerned about this one
this.Operations.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<int, Buffer2D<float>> m in this.glyphMap)
{
m.Value.Dispose();
}
}
public void EndFigure()
{
this.builder.CloseFigure();
}
public void EndGlyph()
{
if (!this.glyphMap.ContainsKey(this.currentRenderingGlyph))
{
this.RenderToCache();
}
this.Operations.Add(new DrawingOperation
{
Location = this.currentRenderPosition,
Map = this.glyphMap[this.currentRenderingGlyph]
});
}
private void RenderToCache()
{
IPath path = this.builder.Build();
var size = Rectangle.Ceiling(path.Bounds);
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.MemoryManager.Allocate2D<float>(size.Width + 1, size.Height + 1, true);
this.glyphMap.Add(this.currentRenderingGlyph, fullBuffer);
using (IBuffer<float> bufferBacking = this.MemoryManager.Allocate<float>(path.MaxIntersections))
using (IBuffer<PointF> rowIntersectionBuffer = this.MemoryManager.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.Span;
Span<float> buffer = bufferBacking.Span;
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(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;
}
}
}
}
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Swap(Span<float> data, int left, int right)
{
float tmp = data[left];
data[left] = data[right];
data[right] = tmp;
}
private static void QuickSort(Span<float> data)
{
QuickSort(data, 0, data.Length - 1);
}
private static void QuickSort(Span<float> data, int lo, int hi)
{
if (lo < hi)
{
int p = Partition(data, lo, hi);
QuickSort(data, lo, p);
QuickSort(data, p + 1, hi);
}
}
private static int Partition(Span<float> data, int lo, int hi)
{
float pivot = data[lo];
int i = lo - 1;
int j = hi + 1;
while (true)
{
do
{
i = i + 1;
}
while (data[i] < pivot && i < hi);
do
{
j = j - 1;
}
while (data[j] > pivot && j > lo);
if (i >= j)
{
return j;
}
Swap(data, i, j);
}
}
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;
}
}
}
}

19
src/ImageSharp.Drawing/Processing/Text/TextGraphicsOptions.cs

@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Processing.Text
/// </summary>
public struct TextGraphicsOptions
{
private const int DefaultTextDpi = 72;
/// <summary>
/// Represents the default <see cref="TextGraphicsOptions"/>.
/// </summary>
@ -26,11 +28,16 @@ namespace SixLabors.ImageSharp.Processing.Text
private float? tabWidth;
private float? dpiX;
private float? dpiY;
private PixelBlenderMode blenderMode;
private float wrapTextWidth;
private HorizontalAlignment? horizontalAlignment;
private VerticalAlignment? verticalAlignment;
/// <summary>
@ -49,6 +56,8 @@ namespace SixLabors.ImageSharp.Processing.Text
this.blenderMode = PixelBlenderMode.Normal;
this.blendPercentage = 1;
this.antialias = enableAntialiasing;
this.dpiX = DefaultTextDpi;
this.dpiY = DefaultTextDpi;
}
/// <summary>
@ -90,6 +99,16 @@ namespace SixLabors.ImageSharp.Processing.Text
/// </summary>
public float WrapTextWidth { get => this.wrapTextWidth; set => this.wrapTextWidth = value; }
/// <summary>
/// Gets or sets a value indicating the DPI to render text along the X axis.
/// </summary>
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; }
/// <summary>
/// Gets or sets a value indicating the DPI to render text along the Y axis.
/// </summary>
public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; }
/// <summary>
/// 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

101
tests/ImageSharp.Benchmarks/Drawing/DrawText.cs

@ -0,0 +1,101 @@
// <copyright file="Crop.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
using System.Drawing;
using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes;
using System.IO;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Text;
using SixLabors.ImageSharp.Processing.Overlays;
using SixLabors.ImageSharp.Processing.Drawing;
using System.Linq;
namespace SixLabors.ImageSharp.Benchmarks
{
[MemoryDiagnoser]
public class DrawText : BenchmarkBase
{
[Params(10, 100)]
public int TextIterations{ get; set; }
public string TextPhrase { get; set; } = "Hello World";
public string TextToRender => string.Join(" ", Enumerable.Repeat(TextPhrase, TextIterations));
[Benchmark(Baseline = true, Description = "System.Drawing Draw Text")]
public void DrawTextSystemDrawing()
{
using (Bitmap destination = new Bitmap(800, 800))
{
using (Graphics graphics = Graphics.FromImage(destination))
{
graphics.InterpolationMode = InterpolationMode.Default;
graphics.SmoothingMode = SmoothingMode.AntiAlias;
Pen pen = new Pen(System.Drawing.Color.HotPink, 10);
var font = new Font("Arial", 12, GraphicsUnit.Point);
graphics.DrawString(TextToRender, font, Brushes.HotPink, new RectangleF(10, 10, 780, 780));
}
}
}
[Benchmark(Description = "ImageSharp Draw Text - Cached Glyphs")]
public void DrawTextCore()
{
using (Image<Rgba32> image = new Image<Rgba32>(800, 800))
{
var font = SixLabors.Fonts.SystemFonts.CreateFont("Arial", 12);
image.Mutate(x => x.ApplyProcessor(new SixLabors.ImageSharp.Processing.Text.Processors.DrawTextProcessor<Rgba32>(new TextGraphicsOptions(true) { WrapTextWidth = 780 }, TextToRender, font, SixLabors.ImageSharp.Processing.Drawing.Brushes.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10))));
}
}
[Benchmark(Description = "ImageSharp Draw Text - Nieve")]
public void DrawTextCoreOld()
{
using (Image<Rgba32> image = new Image<Rgba32>(800, 800))
{
var font = SixLabors.Fonts.SystemFonts.CreateFont("Arial", 12);
image.Mutate(x => DrawTextOldVersion(x, new TextGraphicsOptions(true) { WrapTextWidth = 780 }, TextToRender, font, SixLabors.ImageSharp.Processing.Drawing.Brushes.Brushes.Solid(Rgba32.HotPink), null, new SixLabors.Primitives.PointF(10, 10)));
}
IImageProcessingContext<TPixel> DrawTextOldVersion<TPixel>(IImageProcessingContext<TPixel> source, TextGraphicsOptions options, string text, SixLabors.Fonts.Font font, SixLabors.ImageSharp.Processing.Drawing.Brushes.IBrush<TPixel> brush, SixLabors.ImageSharp.Processing.Drawing.Pens.IPen<TPixel> pen, SixLabors.Primitives.PointF location)
where TPixel : struct, IPixel<TPixel>
{
float dpiX = 72;
float dpiY = 72;
var style = new SixLabors.Fonts.RendererOptions(font, dpiX, dpiY, location)
{
ApplyKerning = options.ApplyKerning,
TabWidth = options.TabWidth,
WrappingWidth = options.WrapTextWidth,
HorizontalAlignment = options.HorizontalAlignment,
VerticalAlignment = options.VerticalAlignment
};
Shapes.IPathCollection glyphs = Shapes.TextBuilder.GenerateGlyphs(text, style);
var pathOptions = (GraphicsOptions)options;
if (brush != null)
{
source.Fill(pathOptions, brush, glyphs);
}
if (pen != null)
{
source.Draw(pathOptions, pen, glyphs);
}
return source;
}
}
}
}

138
tests/ImageSharp.Benchmarks/Drawing/OldProcessors/DrawTextProcessorV1.cs

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Text;
using SixLabors.Fonts;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Drawing.Processors;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.ImageSharp.Processing.Text;
using SixLabors.Primitives;
using SixLabors.Shapes;
namespace SixLabors.ImageSharp.Benchmarks.Drawing.OldProcessors
{
/// <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 FillRegionProcessor<TPixel> fillRegionProcessor = null;
/// <summary>
/// Initializes a new instance of the <see cref="DrawTextProcessor{TPixel}"/> class.
/// </summary>
/// <param name="options">The options</param>
/// <param name="text">The text we want to render</param>
/// <param name="font">The font we want to render with</param>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="pen">The pen to outline text with.</param>
/// <param name="location">The location on the image to start drawign the text from.</param>
public DrawTextProcessor(TextGraphicsOptions options, string text, Font font, IBrush<TPixel> brush, IPen<TPixel> pen, PointF location)
{
this.Brush = brush;
this.Options = options;
this.Text = text;
this.Pen = pen;
this.Font = font;
this.Location = location;
}
/// <summary>
/// Gets or sets the brush.
/// </summary>
public IBrush<TPixel> Brush { get; set; }
/// <summary>
/// Gets or sets the options
/// </summary>
public TextGraphicsOptions Options { get; set; }
/// <summary>
/// Gets or sets the text
/// </summary>
public string Text { get; set; }
/// <summary>
/// Gets or sets the pen used for outlining the text, if Null then we will not outline
/// </summary>
public IPen<TPixel> Pen { get; set; }
/// <summary>
/// Gets or sets the font used to render the text.
/// </summary>
public Font Font { get; set; }
/// <summary>
/// Gets or sets the location to draw the text at.
/// </summary>
public PointF Location { get; set; }
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle)
{
base.BeforeImageApply(source, sourceRectangle);
// do everythign at the image level as we are deligating 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
};
IPathCollection glyphs = TextBuilder.GenerateGlyphs(this.Text, style);
var pathOptions = (GraphicsOptions)this.Options;
if (this.Brush != null)
{
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapeRegion(p);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
if (this.Pen != null)
{
// we will reuse the processor for all fill operations to reduce allocations
if (this.fillRegionProcessor == null)
{
this.fillRegionProcessor = new FillRegionProcessor<TPixel>()
{
Brush = this.Brush,
Options = pathOptions
};
}
foreach (IPath p in glyphs)
{
this.fillRegionProcessor.Region = new ShapePath(p, this.Pen);
this.fillRegionProcessor.Apply(source, sourceRectangle);
}
}
}
/// <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
}
}
}

61
tests/ImageSharp.Tests/Drawing/Text/DrawText.Path.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Drawing.Processors;
using SixLabors.ImageSharp.Processing.Text;
using SixLabors.ImageSharp.Processing.Text.Processors;
using SixLabors.Shapes;
using Xunit;
@ -44,9 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
null,
this.path);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -54,9 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), null, this.path);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -64,9 +61,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Brushes.Solid(Rgba32.Red), this.path);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -74,9 +69,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), this.path);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -84,9 +77,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Rgba32.Red, this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
SolidBrush<Rgba32> brush = Assert.IsType<SolidBrush<Rgba32>>(processor.Brush);
Assert.Equal(Rgba32.Red, brush.Color);
@ -97,9 +88,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Rgba32.Red, this.path);
FillRegionProcessor<Rgba32> processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
DrawTextOnPathProcessor<Rgba32> processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
SolidBrush<Rgba32> brush = Assert.IsType<SolidBrush<Rgba32>>(processor.Brush);
Assert.Equal(Rgba32.Red, brush.Color);
@ -116,9 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
Pens.Dash(Rgba32.Red, 1),
this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -126,9 +113,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, null, Pens.Dash(Rgba32.Red, 1), this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -136,9 +121,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Pens.Dash(Rgba32.Red, 1), this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -146,9 +129,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Pens.Dash(Rgba32.Red, 1), this.path);
FillRegionProcessor<Rgba32> processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
DrawTextOnPathProcessor<Rgba32> processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -162,12 +143,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
Pens.Dash(Rgba32.Red, 1),
this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<FillRegionProcessor<Rgba32>>(3);
this.Verify<FillRegionProcessor<Rgba32>>(4);
this.Verify<FillRegionProcessor<Rgba32>>(5);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -175,12 +151,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), Pens.Dash(Rgba32.Red, 1), this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<FillRegionProcessor<Rgba32>>(3);
this.Verify<FillRegionProcessor<Rgba32>>(4);
this.Verify<FillRegionProcessor<Rgba32>>(5);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -194,8 +165,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
Pens.Dash(Rgba32.Red, 1),
this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
[Fact]
@ -203,8 +173,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("1", this.Font, Brushes.Solid(Rgba32.Red), Pens.Dash(Rgba32.Red, 1), this.path);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
var processor = this.Verify<DrawTextOnPathProcessor<Rgba32>>(0);
}
}
}

101
tests/ImageSharp.Tests/Drawing/Text/DrawText.cs

@ -8,6 +8,8 @@ using SixLabors.ImageSharp.Processing.Drawing.Brushes;
using SixLabors.ImageSharp.Processing.Drawing.Pens;
using SixLabors.ImageSharp.Processing.Drawing.Processors;
using SixLabors.ImageSharp.Processing.Text;
using SixLabors.ImageSharp.Processing.Text.Processors;
using SixLabors.Primitives;
using SixLabors.Shapes;
using Xunit;
@ -44,9 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
null,
Vector2.Zero);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -54,9 +54,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), null, Vector2.Zero);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -64,9 +62,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Brushes.Solid(Rgba32.Red), Vector2.Zero);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -74,9 +70,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), Vector2.Zero);
this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -84,9 +78,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Rgba32.Red, Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
SolidBrush<Rgba32> brush = Assert.IsType<SolidBrush<Rgba32>>(processor.Brush);
Assert.Equal(Rgba32.Red, brush.Color);
@ -97,9 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Rgba32.Red, Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
SolidBrush<Rgba32> brush = Assert.IsType<SolidBrush<Rgba32>>(processor.Brush);
Assert.Equal(Rgba32.Red, brush.Color);
@ -116,9 +106,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
Pens.Dash(Rgba32.Red, 1),
Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -126,9 +114,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, null, Pens.Dash(Rgba32.Red, 1), Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -136,9 +122,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText(new TextGraphicsOptions(true), "123", this.Font, Pens.Dash(Rgba32.Red, 1), Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
}
[Fact]
@ -146,9 +130,14 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
{
this.operations.DrawText("123", this.Font, Pens.Dash(Rgba32.Red, 1), Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
Assert.Equal("123", processor.Text);
Assert.Equal(this.Font, processor.Font);
var penBrush = Assert.IsType<SolidBrush<Rgba32>>(processor.Pen.StrokeFill);
Assert.Equal(Rgba32.Red, penBrush.Color);
Assert.Equal(1, processor.Pen.StrokeWidth);
Assert.Equal(PointF.Empty, processor.Location);
}
[Fact]
@ -162,50 +151,16 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
Pens.Dash(Rgba32.Red, 1),
Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<FillRegionProcessor<Rgba32>>(3);
this.Verify<FillRegionProcessor<Rgba32>>(4);
this.Verify<FillRegionProcessor<Rgba32>>(5);
}
[Fact]
public void DrawForEachACharachterWhenPenSetAndFillFroEachWhenBrushSetDefaultOptions()
{
this.operations.DrawText("123", this.Font, Brushes.Solid(Rgba32.Red), Pens.Dash(Rgba32.Red, 1), Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
this.Verify<FillRegionProcessor<Rgba32>>(2);
this.Verify<FillRegionProcessor<Rgba32>>(3);
this.Verify<FillRegionProcessor<Rgba32>>(4);
this.Verify<FillRegionProcessor<Rgba32>>(5);
}
[Fact]
public void BrushAppliesBeforePen()
{
this.operations.DrawText(
new TextGraphicsOptions(true),
"1",
this.Font,
Brushes.Solid(Rgba32.Red),
Pens.Dash(Rgba32.Red, 1),
Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
}
var processor = this.Verify<DrawTextProcessor<Rgba32>>(0);
[Fact]
public void BrushAppliesBeforPenDefaultOptions()
{
this.operations.DrawText("1", this.Font, Brushes.Solid(Rgba32.Red), Pens.Dash(Rgba32.Red, 1), Vector2.Zero);
var processor = this.Verify<FillRegionProcessor<Rgba32>>(0);
this.Verify<FillRegionProcessor<Rgba32>>(1);
Assert.Equal("123", processor.Text);
Assert.Equal(this.Font, processor.Font);
var brush = Assert.IsType<SolidBrush<Rgba32>>(processor.Brush);
Assert.Equal(Rgba32.Red, brush.Color);
Assert.Equal(PointF.Empty, processor.Location);
var penBrush = Assert.IsType<SolidBrush<Rgba32>>(processor.Pen.StrokeFill);
Assert.Equal(Rgba32.Red, penBrush.Color);
Assert.Equal(1, processor.Pen.StrokeWidth);
}
}
}

22
tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
using System;
using System.Linq;
using System.Text;
using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes;
using SixLabors.Primitives;
[GroupOutput("Drawing/Text")]
@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
private const string TestText2 =
"THISISTESTWORDS ";
[Theory]
[WithSolidFilledImages(200, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "SixLaborsSampleAB.woff", AB)]
[WithSolidFilledImages(900, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "OpenSans-Regular.ttf", TestText)]
@ -51,9 +51,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
provider.VerifyOperation(
img =>
{
img.Mutate(c => c.DrawText(text, new Font(font, fontSize), color, new PointF(x, y)));
},
{
img.Mutate(c => c.DrawText(text, new Font(font, fontSize), color, new PointF(x, y)));
},
$"{fontName}-{fontSize}-{fnDisplayText}-({x},{y})",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: true);
@ -84,12 +84,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text
}
var textOptions = new TextGraphicsOptions
{
Antialias = true,
ApplyKerning = true,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
};
{
Antialias = true,
ApplyKerning = true,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
};
TPixel color = NamedColors<TPixel>.Black;
provider.VerifyOperation(

6
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -45,6 +45,12 @@
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<None Update="TestFonts\OpenSans-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestFonts\SixLaborsSampleAB.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Loading…
Cancel
Save