Skip to content

Commit bbb764e

Browse files
authored
Merge pull request #36 from SixLabors/sw/nonzero-complexpolygons
Render glyphs with nonzero IntersectionRule
2 parents 355758d + 3ca55f6 commit bbb764e

5 files changed

Lines changed: 209 additions & 47 deletions

File tree

src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ void Draw(List<DrawingOperation> operations, IBrush brush)
8787
int startY = operation.Location.Y;
8888
int startX = operation.Location.X;
8989
int offsetSpan = 0;
90+
91+
if (startX + buffer.Height < 0)
92+
{
93+
continue;
94+
}
95+
96+
if (startX + buffer.Width < 0)
97+
{
98+
continue;
99+
}
100+
90101
if (startX < 0)
91102
{
92103
offsetSpan = -startX;
@@ -335,7 +346,7 @@ private Buffer2D<float> Render(IPath path)
335346
{
336347
var start = new PointF(path.Bounds.Left - 1, subPixel);
337348
var end = new PointF(path.Bounds.Right + 1, subPixel);
338-
int pointsFound = path.FindIntersections(start, end, intersectionSpan);
349+
int pointsFound = path.FindIntersections(start, end, intersectionSpan, IntersectionRule.Nonzero);
339350

340351
if (pointsFound == 0)
341352
{

src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Buffers;
56
using System.Collections.Generic;
67
using System.Linq;
78
using System.Numerics;
9+
using Orientation = SixLabors.ImageSharp.Drawing.InternalPath.Orientation;
810

911
namespace SixLabors.ImageSharp.Drawing
1012
{
@@ -15,6 +17,7 @@ namespace SixLabors.ImageSharp.Drawing
1517
public sealed class ComplexPolygon : IPath
1618
{
1719
private readonly IPath[] paths;
20+
private List<InternalPath> internalPaths = null;
1821

1922
/// <summary>
2023
/// Initializes a new instance of the <see cref="ComplexPolygon" /> class.
@@ -190,25 +193,68 @@ public int FindIntersections(PointF start, PointF end, PointF[] buffer, int offs
190193
/// <inheritdoc />
191194
public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, IntersectionRule intersectionRule)
192195
{
196+
this.EnsureInternalPathsInitalized();
197+
193198
int totalAdded = 0;
194-
for (int i = 0; i < this.paths.Length; i++)
199+
Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(buffer.Length); // the largest number of intersections of any sub path of the set is the max size with need for this buffer.
200+
Span<Orientation> orientationsSpan = orientations;
201+
try
195202
{
196-
Span<PointF> subBuffer = buffer.Slice(totalAdded);
197-
int added = this.paths[i].FindIntersections(start, end, subBuffer, intersectionRule);
198-
totalAdded += added;
199-
}
203+
foreach (var ip in this.internalPaths)
204+
{
205+
Span<PointF> subBuffer = buffer.Slice(totalAdded);
206+
Span<Orientation> subOrientationsSpan = orientationsSpan.Slice(totalAdded);
207+
208+
var position = ip.FindIntersectionsWithOrientation(start, end, subBuffer, subOrientationsSpan);
209+
totalAdded += position;
210+
}
200211

201-
Span<float> distances = stackalloc float[totalAdded];
202-
for (int i = 0; i < totalAdded; i++)
212+
Span<float> distances = stackalloc float[totalAdded];
213+
for (int i = 0; i < totalAdded; i++)
214+
{
215+
distances[i] = Vector2.DistanceSquared(start, buffer[i]);
216+
}
217+
218+
var activeBuffer = buffer.Slice(0, totalAdded);
219+
var activeOrientationsSpan = orientationsSpan.Slice(0, totalAdded);
220+
QuickSort.Sort(distances, activeBuffer, activeOrientationsSpan);
221+
222+
if (intersectionRule == IntersectionRule.Nonzero)
223+
{
224+
totalAdded = InternalPath.ApplyNonZeroIntersectionRules(activeBuffer, activeOrientationsSpan);
225+
}
226+
}
227+
finally
203228
{
204-
distances[i] = Vector2.DistanceSquared(start, buffer[i]);
229+
ArrayPool<Orientation>.Shared.Return(orientations);
205230
}
206231

207-
QuickSort.Sort(distances, buffer.Slice(0, totalAdded));
208-
209232
return totalAdded;
210233
}
211234

235+
private void EnsureInternalPathsInitalized()
236+
{
237+
if (this.internalPaths == null)
238+
{
239+
lock (this.paths)
240+
{
241+
if (this.internalPaths == null)
242+
{
243+
this.internalPaths = new List<InternalPath>(this.paths.Length);
244+
245+
foreach (var p in this.paths)
246+
{
247+
foreach (var s in p.Flatten())
248+
{
249+
var ip = new InternalPath(s.Points, s.IsClosed);
250+
this.internalPaths.Add(ip);
251+
}
252+
}
253+
}
254+
}
255+
}
256+
}
257+
212258
/// <summary>
213259
/// Determines whether the <see cref="IPath" /> contains the specified point
214260
/// </summary>

src/ImageSharp.Drawing/Shapes/InternalPath.cs

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private InternalPath(PointData[] points, bool isClosedPath)
8888
/// <summary>
8989
/// the orrientateion of an point form a line
9090
/// </summary>
91-
private enum Orientation
91+
internal enum Orientation
9292
{
9393
/// <summary>
9494
/// Point is colienear
@@ -189,6 +189,40 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer)
189189
/// <param name="intersectionRule">Intersection rule types</param>
190190
/// <returns>number of intersections hit</returns>
191191
public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, IntersectionRule intersectionRule)
192+
{
193+
Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(buffer.Length);
194+
try
195+
{
196+
Span<Orientation> orientationsSpan = orientations.AsSpan(0, buffer.Length);
197+
var position = this.FindIntersectionsWithOrientation(start, end, buffer, orientationsSpan);
198+
199+
var activeBuffer = buffer.Slice(0, position);
200+
var activeOrientationsSpan = orientationsSpan.Slice(0, position);
201+
202+
// intersection rules only really apply to closed paths
203+
if (intersectionRule == IntersectionRule.Nonzero && this.closedPath)
204+
{
205+
position = ApplyNonZeroIntersectionRules(activeBuffer, activeOrientationsSpan);
206+
}
207+
208+
return position;
209+
}
210+
finally
211+
{
212+
ArrayPool<Orientation>.Shared.Return(orientations);
213+
}
214+
}
215+
216+
/// <summary>
217+
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
218+
/// populates a buffer for all points on the path that the line intersects.
219+
/// </summary>
220+
/// <param name="start">The start.</param>
221+
/// <param name="end">The end.</param>
222+
/// <param name="buffer">The buffer.</param>
223+
/// <param name="orientationsSpan">The buffer for storeing the orientation of each intersection.</param>
224+
/// <returns>number of intersections hit</returns>
225+
public int FindIntersectionsWithOrientation(PointF start, PointF end, Span<PointF> buffer, Span<Orientation> orientationsSpan)
192226
{
193227
if (this.points.Length < 2)
194228
{
@@ -212,8 +246,7 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, Inte
212246
Vector2 lastPoint = MaxVector;
213247

214248
PassPointData[] precaclulate = ArrayPool<PassPointData>.Shared.Rent(this.points.Length);
215-
Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(this.points.Length);
216-
Span<Orientation> orientationsSpan = orientations.AsSpan(0, this.points.Length);
249+
217250
Span<PassPointData> precaclulateSpan = precaclulate.AsSpan(0, this.points.Length);
218251

219252
try
@@ -341,48 +374,52 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, Inte
341374
distances[i] = Vector2.DistanceSquared(startVector, buffer[i]);
342375
}
343376

344-
QuickSort.Sort(distances, buffer.Slice(0, position), orientationsSpan.Slice(0, position));
345-
346-
// intersection rules only really apply to closed paths
347-
if (intersectionRule == IntersectionRule.Nonzero && this.closedPath)
348-
{
349-
int newpositions = 0;
350-
int tracker = 0;
351-
for (int i = 0; i < position; i++)
352-
{
353-
bool include = tracker == 0;
354-
switch (orientationsSpan[i])
355-
{
356-
case Orientation.Clockwise:
357-
tracker++;
358-
break;
359-
case Orientation.Counterclockwise:
360-
tracker--;
361-
break;
362-
case Orientation.Colinear:
363-
default:
364-
break;
365-
}
366-
367-
if (include || tracker == 0)
368-
{
369-
buffer[newpositions] = buffer[i];
370-
newpositions++;
371-
}
372-
}
373-
374-
position = newpositions;
375-
}
377+
var activeBuffer = buffer.Slice(0, position);
378+
var activeOrientationsSpan = orientationsSpan.Slice(0, position);
379+
QuickSort.Sort(distances, activeBuffer, activeOrientationsSpan);
376380

377381
return position;
378382
}
379383
finally
380384
{
381-
ArrayPool<Orientation>.Shared.Return(orientations);
382385
ArrayPool<PassPointData>.Shared.Return(precaclulate);
383386
}
384387
}
385388

389+
internal static int ApplyNonZeroIntersectionRules(Span<PointF> buffer, Span<Orientation> orientationsSpan)
390+
{
391+
int newpositions = 0;
392+
int tracker = 0;
393+
int diff = 0;
394+
for (int i = 0; i < buffer.Length; i++)
395+
{
396+
bool include = tracker == 0;
397+
switch (orientationsSpan[i])
398+
{
399+
case Orientation.Counterclockwise:
400+
diff = 1;
401+
break;
402+
case Orientation.Clockwise:
403+
diff = -1;
404+
break;
405+
case Orientation.Colinear:
406+
default:
407+
diff *= -1;
408+
break;
409+
}
410+
411+
tracker += diff;
412+
413+
if (include || tracker == 0)
414+
{
415+
buffer[newpositions] = buffer[i];
416+
newpositions++;
417+
}
418+
}
419+
420+
return newpositions;
421+
}
422+
386423
/// <summary>
387424
/// Determines if the specified point is inside or outside the path.
388425
/// </summary>

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2<TPixel>(Test
8383
}
8484

8585

86+
[Theory]
87+
[WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32)]
88+
public void OpenSansJWithNoneZeroShouldntExtendPastGlyphe<TPixel>(TestImageProvider<TPixel> provider)
89+
where TPixel : unmanaged, IPixel<TPixel>
90+
{
91+
using (Image<TPixel> img = provider.GetImage())
92+
{
93+
Font font = CreateFont("OpenSans-Regular.ttf", 50);
94+
Color color = Color.Black;
95+
96+
img.Mutate(ctx => ctx.DrawText(TestText, font, Color.Black, new PointF(-50, 2)));
97+
98+
Assert.Equal(Color.White.ToPixel<TPixel>(), img[173, 2]);
99+
}
100+
}
101+
86102
[Theory]
87103
[WithSolidFilledImages(200, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "SixLaborsSampleAB.woff", AB)]
88104
[WithSolidFilledImages(900, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "OpenSans-Regular.ttf", TestText)]

tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,58 @@ public ComplexPolygonTests(ITestOutputHelper output)
4242

4343
private ITestOutputHelper Output { get; }
4444

45+
[Fact]
46+
public void NonezeroFindIntersections()
47+
{
48+
var simplePath1 = new Polygon(new LinearLineSegment(
49+
new PointF(2, 1),
50+
new PointF(2, 5),
51+
new PointF(3, 5),
52+
new PointF(3, 1)));
53+
54+
var simplePath2 = new Polygon(new LinearLineSegment(
55+
new PointF(1, 2),
56+
new PointF(1, 3),
57+
new PointF(4, 3),
58+
new PointF(4, 2)));
59+
60+
var complex = new ComplexPolygon(simplePath1, simplePath2);
61+
62+
var buffer = new PointF[10];
63+
var points = complex.FindIntersections(new PointF(0, 2.5f), new PointF(6, 2.5f), buffer, 0, IntersectionRule.Nonzero);
64+
65+
Assert.Equal(2, points);
66+
Assert.Equal(1, buffer[0].X);
67+
Assert.Equal(4, buffer[1].X);
68+
}
69+
70+
[Fact]
71+
public void OddEvenFindIntersections()
72+
{
73+
var simplePath1 = new Polygon(new LinearLineSegment(
74+
new PointF(2, 1),
75+
new PointF(2, 5),
76+
new PointF(3, 5),
77+
new PointF(3, 1)));
78+
79+
var simplePath2 = new Polygon(new LinearLineSegment(
80+
new PointF(1, 2),
81+
new PointF(1, 3),
82+
new PointF(4, 3),
83+
new PointF(4, 2)));
84+
85+
var complex = new ComplexPolygon(simplePath1, simplePath2);
86+
87+
var buffer = new PointF[10];
88+
var points = complex.FindIntersections(new PointF(0, 2.5f), new PointF(6, 2.5f), buffer, 0, IntersectionRule.OddEven);
89+
90+
Assert.Equal(4, points);
91+
Assert.Equal(1, buffer[0].X);
92+
Assert.Equal(2, buffer[1].X);
93+
Assert.Equal(3, buffer[2].X);
94+
Assert.Equal(4, buffer[3].X);
95+
}
96+
4597
[Fact]
4698
public void MissingIntersection()
4799
{

0 commit comments

Comments
 (0)