Skip to content

Commit 11a0061

Browse files
Better layout for both horizontal and vertical.
1 parent 9c8d9df commit 11a0061

5 files changed

Lines changed: 79 additions & 130 deletions

File tree

samples/DrawShapesWithImageSharp/Program.cs

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ private static void OutputStars()
5252
DrawFatL();
5353

5454
DrawText("Hello World");
55+
5556
DrawText(
5657
"Hello World Hello World Hello World Hello World Hello World Hello World Hello World",
57-
new EllipsePolygon(PointF.Empty, 100));
58+
new EllipsePolygon(PointF.Empty, 100));
59+
// new RectangularPolygon(PointF.Empty, new SizeF(100, 100)));
5860
}
5961

6062
private static void DrawText(string text)
@@ -69,21 +71,21 @@ private static void DrawText(string text)
6971

7072
private static void DrawText(string text, IPath path)
7173
{
72-
FontFamily fam = SystemFonts.Get("Arial");
73-
var font = new Font(fam, 30);
74+
FontFamily family = SystemFonts.Get("Arial");
75+
Font font = new(family, 30);
7476
TextOptions textOptions = new(font)
7577
{
7678
WrappingLength = path.ComputeLength(),
7779
VerticalAlignment = VerticalAlignment.Top,
7880
HorizontalAlignment = HorizontalAlignment.Left,
7981

8082
// Enable this to test vertical layout mode.
81-
LayoutMode = LayoutMode.VerticalLeftRight
83+
//LayoutMode = LayoutMode.VerticalLeftRight
8284
};
8385

8486
IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, path, textOptions);
8587

86-
glyphs.SaveImage("Text-Path", text + ".png");
88+
glyphs.SaveImageWithPath(path, "Text-Path", text + ".png");
8789
}
8890

8991
private static void DrawFatL()
@@ -223,61 +225,67 @@ private static void OutputClippedRectangle()
223225

224226
public static void SaveImage(this IPath shape, params string[] path) => new PathCollection(shape).SaveImage(path);
225227

226-
public static void SaveImage(this IPathCollection shape, params string[] path)
228+
public static void SaveImage(this IPathCollection collection, params string[] path)
227229
{
228-
shape = shape.Translate(-shape.Bounds.Location) // touch top left
229-
.Translate(new Vector2(10)); // move in from top left
230+
// Offset the path collection to ensure our resultant image is
231+
// large enough to contain the rendered output.
232+
collection = collection.Translate(-collection.Bounds.Location);
233+
234+
int width = (int)(collection.Bounds.Left + collection.Bounds.Right);
235+
int height = (int)(collection.Bounds.Top + collection.Bounds.Bottom);
236+
using var img = new Image<Rgba32>(width, height);
237+
238+
// Fill the canvas background and draw our shape
239+
img.Mutate(i => i.Fill(Color.DarkBlue));
230240

241+
// Draw our path collection.
242+
img.Mutate(i => i.Fill(Color.HotPink, collection));
243+
244+
// Ensure directory exists
231245
string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path)));
246+
IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
247+
img.Save(fullPath);
248+
}
232249

233-
// pad even amount around shape
234-
int width = (int)(shape.Bounds.Left + shape.Bounds.Right);
235-
int height = (int)(shape.Bounds.Top + shape.Bounds.Bottom);
250+
public static void SaveImageWithPath(this IPathCollection collection, IPath shape, params string[] path)
251+
{
252+
// Offset the shape and path collection to ensure our resultant image is
253+
// large enough to contain the rendered output.
254+
shape = shape.Translate(-collection.Bounds.Location);
255+
collection = collection.Translate(-collection.Bounds.Location);
236256

237-
using (var img = new Image<Rgba32>(width, height))
238-
{
239-
img.Mutate(i => i.Fill(Color.DarkBlue).Draw(Color.HotPink, 3, new EllipsePolygon(width / 2F, height / 2F, 93)));
257+
var bounds = RectangleF.Union(shape.Bounds, collection.Bounds);
258+
int width = (int)(bounds.Left + bounds.Right);
259+
int height = (int)(bounds.Top + bounds.Bottom);
240260

241-
foreach (IPath s in shape)
242-
{
243-
// In ImageSharp.Drawing.Paths there is an extension method that takes in an IShape directly.
244-
img.Mutate(i => i.Fill(Color.HotPink, s));
245-
}
261+
using var img = new Image<Rgba32>(width, height);
246262

247-
// img.Draw(Color.LawnGreen, 1, new ShapePath(shape));
263+
// Fill the canvas background and draw our shape
264+
img.Mutate(i => i.Fill(Color.DarkBlue).Fill(Color.White.WithAlpha(.25F), shape));
248265

249-
// Ensure directory exists
250-
IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
266+
// Draw our path collection.
267+
img.Mutate(i => i.Fill(Color.HotPink, collection));
251268

252-
img.Save(fullPath);
253-
}
269+
// Ensure directory exists
270+
string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path)));
271+
IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
272+
img.Save(fullPath);
254273
}
255274

256275
public static void SaveImage(this IPath shape, int width, int height, params string[] path)
257276
=> new PathCollection(shape).SaveImage(width, height, path);
258277

259278
public static void SaveImage(this IPathCollection shape, int width, int height, params string[] path)
260279
{
261-
string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path)));
262-
263-
using (var img = new Image<Rgba32>(width, height))
264-
{
265-
img.Mutate(i => i.Fill(Color.DarkBlue));
280+
using var img = new Image<Rgba32>(width, height);
281+
img.Mutate(i => i.Fill(Color.DarkBlue));
282+
img.Mutate(i => i.Fill(Color.HotPink, shape));
266283

267-
// In ImageSharp.Drawing.Paths there is an extension method that takes in an IShape directly.
268-
foreach (IPath s in shape)
269-
{
270-
// In ImageSharp.Drawing.Paths there is an extension method that takes in an IShape directly.
271-
img.Mutate(i => i.Fill(Color.HotPink, s));
272-
}
273-
274-
// img.Draw(Color.LawnGreen, 1, new ShapePath(shape));
275-
276-
// Ensure directory exists
277-
IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
284+
// Ensure directory exists
285+
string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path)));
286+
IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
278287

279-
img.Save(fullPath);
280-
}
288+
img.Save(fullPath);
281289
}
282290
}
283291
}

src/ImageSharp.Drawing/Shapes/SegmentInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
namespace SixLabors.ImageSharp.Drawing
@@ -14,7 +14,7 @@ public struct SegmentInfo
1414
public PointF Point;
1515

1616
/// <summary>
17-
/// The angle of the segment.
17+
/// The angle of the segment. Measured in radians.
1818
/// </summary>
1919
public float Angle;
2020
}

src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,13 @@ internal class BaseGlyphBuilder : IGlyphRenderer
1919
// ReSharper disable once InconsistentNaming
2020
protected readonly PathBuilder builder;
2121
#pragma warning restore SA1401 // Fields should be private
22-
private readonly List<IPath> paths = new List<IPath>();
22+
private readonly List<IPath> paths = new();
2323
private Vector2 currentPoint = default;
2424

2525
/// <summary>
2626
/// Initializes a new instance of the <see cref="BaseGlyphBuilder"/> class.
2727
/// </summary>
28-
public BaseGlyphBuilder()
29-
{
30-
// glyphs are renderd realative to bottom left so invert the Y axis to allow it to render on top left origin surface
31-
this.builder = new PathBuilder();
32-
}
28+
public BaseGlyphBuilder() => this.builder = new PathBuilder();
3329

3430
/// <summary>
3531
/// Gets the paths that have been rendered by this.
@@ -42,10 +38,7 @@ void IGlyphRenderer.EndText()
4238
}
4339

4440
/// <inheritdoc/>
45-
void IGlyphRenderer.BeginText(FontRectangle bounds)
46-
{
47-
this.BeginText(bounds);
48-
}
41+
void IGlyphRenderer.BeginText(FontRectangle bounds) => this.BeginText(bounds);
4942

5043
/// <inheritdoc/>
5144
bool IGlyphRenderer.BeginGlyph(FontRectangle bounds, GlyphRendererParameters paramaters)
@@ -58,10 +51,7 @@ bool IGlyphRenderer.BeginGlyph(FontRectangle bounds, GlyphRendererParameters par
5851
/// <summary>
5952
/// Begins the figure.
6053
/// </summary>
61-
void IGlyphRenderer.BeginFigure()
62-
{
63-
this.builder.StartFigure();
64-
}
54+
void IGlyphRenderer.BeginFigure() => this.builder.StartFigure();
6555

6656
/// <summary>
6757
/// Draws a cubic bezier from the current point to the <paramref name="point"/>
@@ -78,18 +68,12 @@ void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdContr
7868
/// <summary>
7969
/// Ends the glyph.
8070
/// </summary>
81-
void IGlyphRenderer.EndGlyph()
82-
{
83-
this.paths.Add(this.builder.Build());
84-
}
71+
void IGlyphRenderer.EndGlyph() => this.paths.Add(this.builder.Build());
8572

8673
/// <summary>
8774
/// Ends the figure.
8875
/// </summary>
89-
void IGlyphRenderer.EndFigure()
90-
{
91-
this.builder.CloseFigure();
92-
}
76+
void IGlyphRenderer.EndFigure() => this.builder.CloseFigure();
9377

9478
/// <summary>
9579
/// Draws a line from the current point to the <paramref name="point"/>.
@@ -123,14 +107,14 @@ void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
123107
}
124108

125109
/// <summary>Called before any glyphs have been rendered.</summary>
126-
/// <param name="rect">The bounds the text will be rendered at and at whats size.</param>
127-
protected virtual void BeginText(FontRectangle rect)
110+
/// <param name="bounds">The bounds the text will be rendered at and at what size.</param>
111+
protected virtual void BeginText(FontRectangle bounds)
128112
{
129113
}
130114

131115
/// <summary>Begins the glyph.</summary>
132-
/// <param name="rect">The bounds the glyph will be rendered at and at what size.</param>
133-
protected virtual void BeginGlyph(FontRectangle rect)
116+
/// <param name="bounds">The bounds the glyph will be rendered at and at what size.</param>
117+
protected virtual void BeginGlyph(FontRectangle bounds)
134118
{
135119
}
136120
}

src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ internal sealed class PathGlyphBuilder : GlyphBuilder
1515
{
1616
private const float Pi = MathF.PI;
1717
private readonly IPathInternals path;
18-
private readonly bool isVerticalLayout;
1918
private float xOffset;
2019
private float yOffset;
2120

2221
/// <summary>
2322
/// Initializes a new instance of the <see cref="PathGlyphBuilder"/> class.
2423
/// </summary>
2524
/// <param name="path">The path to render the glyphs along.</param>
26-
/// <param name="layoutMode">The mode to determine the layout of the text.</param>
27-
public PathGlyphBuilder(IPath path, LayoutMode layoutMode)
25+
public PathGlyphBuilder(IPath path)
2826
{
2927
if (path is IPathInternals internals)
3028
{
@@ -34,76 +32,35 @@ public PathGlyphBuilder(IPath path, LayoutMode layoutMode)
3432
{
3533
this.path = new ComplexPolygon(path);
3634
}
37-
38-
this.isVerticalLayout = IsVertical(layoutMode);
3935
}
4036

4137
/// <inheritdoc/>
42-
protected override void BeginText(FontRectangle rect)
38+
protected override void BeginText(FontRectangle bounds)
4339
{
44-
// TODO: This uses the baseline of the text, should it be the bottom?
45-
this.yOffset = rect.Height;
46-
this.xOffset = rect.Left;
40+
this.yOffset = bounds.Bottom;
41+
this.xOffset = bounds.Left;
4742
}
4843

4944
/// <inheritdoc/>
50-
protected override void BeginGlyph(FontRectangle rect)
51-
{
52-
// https://svgwg.org/svg2-draft/text.html#TextpathLayoutRules
53-
if (this.isVerticalLayout)
54-
{
55-
this.TransformGlyphVertical(rect);
56-
}
57-
else
58-
{
59-
this.TransformGlyphHorizontal(rect);
60-
}
61-
}
62-
63-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
64-
private void TransformGlyphHorizontal(FontRectangle rect)
65-
{
66-
// Find the intersection point. Bottom-centre for horizontal text.
67-
float halfWidth = rect.Width * .5F;
68-
Vector2 intersectionPoint = new(rect.Left + halfWidth, rect.Bottom);
69-
70-
// Find the point of this intersection along the given path. This ensures the correct rotation.
71-
SegmentInfo point = this.path.PointAlongPath(intersectionPoint.X - this.xOffset);
72-
73-
// Now offset our target point since we're aligning top-left.
74-
Vector2 targetPoint = point.Point + new PointF(-halfWidth, intersectionPoint.Y - rect.Height - this.yOffset);
75-
76-
// Due to how matrix combining works you have to combine this in the reverse order of operation
77-
// this one rotates the glype then moves it.
78-
Matrix3x2 matrix = Matrix3x2.CreateTranslation(targetPoint - rect.Location) * Matrix3x2.CreateRotation(point.Angle - Pi, point.Point);
79-
this.builder.SetTransform(matrix);
80-
}
45+
protected override void BeginGlyph(FontRectangle bounds) => this.TransformGlyph(bounds);
8146

8247
[MethodImpl(MethodImplOptions.AggressiveInlining)]
83-
private void TransformGlyphVertical(FontRectangle rect)
48+
private void TransformGlyph(FontRectangle bounds)
8449
{
85-
// TODO: Fix this.
86-
// Find the intersection point. Centre-centre for vertical text.
87-
float halfWidth = rect.Width * .5F;
88-
float halfHeight = rect.Height * .5F;
89-
Vector2 intersectionPoint = new(rect.Left + halfWidth, rect.Top + halfHeight);
50+
// Find the intersection point. This should be offset to ensure we rotate at the center of the glyph.
51+
float halfWidth = (bounds.Right - bounds.Left) * .5F;
52+
Vector2 intersectPoint = new(bounds.Left + halfWidth, bounds.Top);
9053

91-
SegmentInfo point = this.path.PointAlongPath(intersectionPoint.X - this.xOffset);
54+
// Find the point of this intersection along the given path.
55+
SegmentInfo pathPoint = this.path.PointAlongPath(intersectPoint.X - this.xOffset);
9256

93-
// Now offset our target point since we're aligning top-left.
94-
Vector2 targetPoint = point.Point + new PointF(-halfWidth, intersectionPoint.Y - halfHeight - this.yOffset);
57+
// Now offset our target point since we're aligning the bottom-left location of our glyph against the path.
58+
Vector2 targetPoint = pathPoint.Point + new PointF(-halfWidth, intersectPoint.Y - this.yOffset);
9559

96-
// Due to how matrix combining works you have to combine this in the reverse order of operation
97-
// this one rotates the glype then moves it.
98-
Matrix3x2 matrix = Matrix3x2.CreateTranslation(targetPoint - rect.Location) * Matrix3x2.CreateRotation(point.Angle - Pi, point.Point);
60+
// Due to how matrix combining works you have to combine this in the reverse order of operation.
61+
// First rotate the glyph then move it.
62+
Matrix3x2 matrix = Matrix3x2.CreateTranslation(targetPoint - bounds.Location) * Matrix3x2.CreateRotation(pathPoint.Angle - Pi, pathPoint.Point);
9963
this.builder.SetTransform(matrix);
10064
}
101-
102-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
103-
private static bool IsVertical(LayoutMode mode)
104-
{
105-
const LayoutMode vertical = LayoutMode.VerticalLeftRight | LayoutMode.VerticalRightLeft;
106-
return (mode & vertical) > 0;
107-
}
10865
}
10966
}

src/ImageSharp.Drawing/Shapes/Text/TextBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static IPathCollection GenerateGlyphs(string text, TextOptions textOption
3636
/// <returns>The <see cref="IPathCollection"/></returns>
3737
public static IPathCollection GenerateGlyphs(string text, IPath path, TextOptions textOptions)
3838
{
39-
PathGlyphBuilder glyphBuilder = new(path, textOptions.LayoutMode);
39+
PathGlyphBuilder glyphBuilder = new(path);
4040
TextRenderer renderer = new(glyphBuilder);
4141

4242
renderer.RenderText(text, textOptions);

0 commit comments

Comments
 (0)