This commit is contained in:
Eliot Jones
2019-12-17 17:33:20 +00:00
9 changed files with 956 additions and 37 deletions

View File

@@ -0,0 +1,173 @@
using System;
using UglyToad.PdfPig.Geometry;
using Xunit;
namespace UglyToad.PdfPig.Tests.Geometry
{
public class PdfLineTests
{
[Fact]
public void OriginIsZero()
{
var origin = new PdfLine();
Assert.Equal(0, origin.Point1.X);
Assert.Equal(0, origin.Point1.Y);
Assert.Equal(0, origin.Point2.X);
Assert.Equal(0, origin.Point2.Y);
}
[Fact]
public void Length()
{
var line = new PdfLine(2, 1, 6, 4);
Assert.Equal(5m, line.Length);
var line2 = new PdfLine(-2, 8, -7, -5);
Assert.Equal(13.93m, Math.Round(line2.Length, 2));
}
[Fact]
public void Contains()
{
var line = new PdfLine(10, 7.5m, 26.3m, 12);
Assert.False(line.Contains(new PdfPoint(5, 2)));
Assert.False(line.Contains(new PdfPoint(5, 6.11963190184049m)));
Assert.False(line.Contains(new PdfPoint(27, 12.1932515337423m)));
Assert.False(line.Contains(new PdfPoint(12, 15)));
Assert.False(line.Contains(new PdfPoint(10, 12)));
Assert.True(line.Contains(new PdfPoint(20, 10.260736196319m)));
Assert.True(line.Contains(new PdfPoint(10, 7.5m)));
var verticalLine = new PdfLine(10, 7.5m, 10, 15);
Assert.False(verticalLine.Contains(new PdfPoint(5, 2)));
Assert.False(verticalLine.Contains(new PdfPoint(12, 15)));
Assert.False(verticalLine.Contains(new PdfPoint(10, 16)));
Assert.False(verticalLine.Contains(new PdfPoint(10, 7)));
Assert.True(verticalLine.Contains(new PdfPoint(10, 12)));
Assert.True(verticalLine.Contains(new PdfPoint(10, 7.5m)));
var horizontalLine = new PdfLine(10, 7.5m, 26.3m, 7.5m);
Assert.False(horizontalLine.Contains(new PdfPoint(5, 2)));
Assert.False(horizontalLine.Contains(new PdfPoint(5, 7.5)));
Assert.False(horizontalLine.Contains(new PdfPoint(27, 7.5)));
Assert.False(horizontalLine.Contains(new PdfPoint(10, 12)));
Assert.True(horizontalLine.Contains(new PdfPoint(20, 7.5)));
Assert.True(horizontalLine.Contains(new PdfPoint(26.3m, 7.5m)));
}
[Fact]
public void ParallelTo()
{
var verticalLine1 = new PdfLine(10, 7.5m, 10, 15);
var verticalLine2 = new PdfLine(200, 0, 200, 551.5467m);
var horizontalLine1 = new PdfLine(10, 7.5m, 26.3m, 7.5m);
var horizontalLine2 = new PdfLine(27, 57, 200.9999872m, 57);
var obliqueLine1 = new PdfLine(10, 7.5m, 26.3m, 12);
var obliqueLine2 = new PdfLine(60, 28.8036809815951m, 40, 23.2822085889571m);
Assert.True(verticalLine1.ParallelTo(verticalLine2));
Assert.True(verticalLine2.ParallelTo(verticalLine1));
Assert.False(obliqueLine1.ParallelTo(verticalLine2));
Assert.False(verticalLine2.ParallelTo(obliqueLine1));
Assert.False(obliqueLine1.ParallelTo(verticalLine1));
Assert.False(verticalLine1.ParallelTo(obliqueLine1));
Assert.True(horizontalLine1.ParallelTo(horizontalLine2));
Assert.True(horizontalLine2.ParallelTo(horizontalLine1));
Assert.False(obliqueLine1.ParallelTo(horizontalLine1));
Assert.False(horizontalLine1.ParallelTo(obliqueLine1));
Assert.False(obliqueLine1.ParallelTo(horizontalLine2));
Assert.False(horizontalLine2.ParallelTo(obliqueLine1));
Assert.False(verticalLine1.ParallelTo(horizontalLine2));
Assert.False(horizontalLine2.ParallelTo(verticalLine1));
Assert.False(verticalLine2.ParallelTo(horizontalLine2));
Assert.False(horizontalLine2.ParallelTo(verticalLine2));
Assert.True(obliqueLine1.ParallelTo(obliqueLine2));
Assert.True(obliqueLine2.ParallelTo(obliqueLine1));
}
[Fact]
public void IntersectsWithLine()
{
var verticalLine1 = new PdfLine(10, 7.5m, 10, 15);
var verticalLine2 = new PdfLine(200, 0, 200, 551.5467m);
var horizontalLine1 = new PdfLine(10, 7.5m, 26.3m, 7.5m);
var horizontalLine2 = new PdfLine(27, 57, 200.9999872m, 57);
var horizontalLine3 = new PdfLine(27, 57, 250, 57);
var obliqueLine1 = new PdfLine(10, 7.5m, 26.3m, 12);
var obliqueLine2 = new PdfLine(60, 28.8036809815951m, 40, 23.2822085889571m);
var obliqueLine3 = new PdfLine(20, 7.5m, 10, 15);
Assert.False(verticalLine1.IntersectsWith(verticalLine2));
Assert.False(verticalLine2.IntersectsWith(verticalLine1));
Assert.False(horizontalLine1.IntersectsWith(horizontalLine2));
Assert.False(horizontalLine2.IntersectsWith(horizontalLine1));
Assert.False(obliqueLine1.IntersectsWith(obliqueLine2));
Assert.False(obliqueLine2.IntersectsWith(obliqueLine1));
Assert.False(obliqueLine1.IntersectsWith(obliqueLine1));
Assert.False(obliqueLine1.IntersectsWith(verticalLine2));
Assert.False(verticalLine2.IntersectsWith(obliqueLine1));
Assert.False(obliqueLine1.IntersectsWith(horizontalLine2));
Assert.False(horizontalLine2.IntersectsWith(obliqueLine1));
Assert.False(verticalLine1.IntersectsWith(horizontalLine2));
Assert.False(horizontalLine2.IntersectsWith(verticalLine1));
Assert.True(obliqueLine1.IntersectsWith(horizontalLine1));
Assert.True(horizontalLine1.IntersectsWith(obliqueLine1));
Assert.True(obliqueLine1.IntersectsWith(verticalLine1));
Assert.True(verticalLine1.IntersectsWith(obliqueLine1));
Assert.True(verticalLine2.IntersectsWith(horizontalLine2));
Assert.True(horizontalLine2.IntersectsWith(verticalLine2));
Assert.True(verticalLine2.IntersectsWith(horizontalLine3));
Assert.True(horizontalLine3.IntersectsWith(verticalLine2));
Assert.True(obliqueLine1.IntersectsWith(obliqueLine3));
Assert.True(obliqueLine3.IntersectsWith(obliqueLine1));
}
[Fact]
public void IntersectLine()
{
var verticalLine1 = new PdfLine(10, 7.5m, 10, 15);
var verticalLine2 = new PdfLine(200, 0, 200, 551.5467m);
var horizontalLine1 = new PdfLine(10, 7.5m, 26.3m, 7.5m);
var horizontalLine2 = new PdfLine(27, 57, 200.9999872m, 57);
var horizontalLine3 = new PdfLine(27, 57, 250, 57);
var obliqueLine1 = new PdfLine(10, 7.5m, 26.3m, 12);
var obliqueLine2 = new PdfLine(60, 28.8036809815951m, 40, 23.2822085889571m);
var obliqueLine3 = new PdfLine(20, 7.5m, 10, 15);
Assert.Null(verticalLine1.Intersect(verticalLine2));
Assert.Null(verticalLine2.Intersect(verticalLine1));
Assert.Null(horizontalLine1.Intersect(horizontalLine2));
Assert.Null(horizontalLine2.Intersect(horizontalLine1));
Assert.Null(obliqueLine1.Intersect(obliqueLine2));
Assert.Null(obliqueLine2.Intersect(obliqueLine1));
Assert.Null(obliqueLine1.Intersect(obliqueLine1));
Assert.Null(obliqueLine1.Intersect(verticalLine2));
Assert.Null(verticalLine2.Intersect(obliqueLine1));
Assert.Null(obliqueLine1.Intersect(horizontalLine2));
Assert.Null(horizontalLine2.Intersect(obliqueLine1));
Assert.Null(verticalLine1.Intersect(horizontalLine2));
Assert.Null(horizontalLine2.Intersect(verticalLine1));
Assert.Equal(new PdfPoint(10, 7.5m), obliqueLine1.Intersect(horizontalLine1));
Assert.Equal(new PdfPoint(10, 7.5m), horizontalLine1.Intersect(obliqueLine1));
Assert.Equal(new PdfPoint(10, 7.5m), obliqueLine1.Intersect(verticalLine1));
Assert.Equal(new PdfPoint(10, 7.5m), verticalLine1.Intersect(obliqueLine1));
Assert.Equal(new PdfPoint(200, 57), verticalLine2.Intersect(horizontalLine2));
Assert.Equal(new PdfPoint(200, 57), horizontalLine2.Intersect(verticalLine2));
Assert.Equal(new PdfPoint(200, 57), verticalLine2.Intersect(horizontalLine3));
Assert.Equal(new PdfPoint(200, 57), horizontalLine3.Intersect(verticalLine2));
Assert.Equal(new PdfPoint(17.3094170403587m, 9.51793721973094m), obliqueLine1.Intersect(obliqueLine3));
Assert.Equal(new PdfPoint(17.3094170403587m, 9.51793721973094m), obliqueLine3.Intersect(obliqueLine1));
}
}
}

View File

@@ -0,0 +1,66 @@
using UglyToad.PdfPig.Geometry;
using Xunit;
namespace UglyToad.PdfPig.Tests.Geometry
{
public class PdfRectangleTests
{
public void Area()
{
PdfRectangle rectangle = new PdfRectangle(10, 10, 20, 20);
Assert.Equal(100m, rectangle.Area);
PdfRectangle rectangle1 = new PdfRectangle(149.95376m, 687.13456m, 451.73539m, 1478.4997m);
Assert.Equal(238819.4618743782m, rectangle1.Area);
}
public void Centroid()
{
PdfRectangle rectangle = new PdfRectangle(10, 10, 20, 20);
Assert.Equal(new PdfPoint(15, 15), rectangle.Centroid);
PdfRectangle rectangle1 = new PdfRectangle(149.95376m, 687.13456m, 451.73539m, 1478.4997m);
Assert.Equal(new PdfPoint(300.844575m, 1082.81713m), rectangle1.Centroid);
}
public void Intersect()
{
PdfRectangle rectangle = new PdfRectangle(10, 10, 20, 20);
PdfRectangle rectangle1 = new PdfRectangle(149.95376m, 687.13456m, 451.73539m, 1478.4997m);
Assert.Null(rectangle.Intersect(rectangle1));
Assert.Equal(rectangle1, rectangle1.Intersect(rectangle1));
PdfRectangle rectangle2 = new PdfRectangle(50, 687.13456m, 350, 1478.4997m);
Assert.Equal(new PdfRectangle(149.95376m, 687.13456m, 350, 1478.4997m), rectangle1.Intersect(rectangle2));
PdfRectangle rectangle3 = new PdfRectangle(200, 800, 350, 1200);
Assert.Equal(rectangle3, rectangle1.Intersect(rectangle3));
}
public void IntersectsWith()
{
PdfRectangle rectangle = new PdfRectangle(10, 10, 20, 20);
PdfRectangle rectangle1 = new PdfRectangle(149.95376m, 687.13456m, 451.73539m, 1478.4997m);
Assert.False(rectangle.IntersectsWith(rectangle1));
Assert.True(rectangle1.IntersectsWith(rectangle1));
PdfRectangle rectangle2 = new PdfRectangle(50, 687.13456m, 350, 1478.4997m);
Assert.True(rectangle1.IntersectsWith(rectangle2));
PdfRectangle rectangle3 = new PdfRectangle(200, 800, 350, 1200);
Assert.True(rectangle1.IntersectsWith(rectangle3));
PdfRectangle rectangle4 = new PdfRectangle(5, 7, 10, 25);
Assert.False(rectangle1.IntersectsWith(rectangle4)); // special case where they share one border
}
public void Contains()
{
PdfRectangle rectangle = new PdfRectangle(10, 10, 20, 20);
Assert.True(rectangle.Contains(new PdfPoint(15, 15)));
Assert.False(rectangle.Contains(new PdfPoint(10, 15)));
Assert.True(rectangle.Contains(new PdfPoint(10, 15), true));
Assert.False(rectangle.Contains(new PdfPoint(100, 100), true));
}
}
}

View File

@@ -103,6 +103,7 @@
"UglyToad.PdfPig.Fonts.FontDescriptorFlags",
"UglyToad.PdfPig.Fonts.FontStretch",
"UglyToad.PdfPig.Fonts.Standard14Font",
"UglyToad.PdfPig.Geometry.GeometryExtensions",
"UglyToad.PdfPig.Geometry.PdfPath",
"UglyToad.PdfPig.Geometry.PdfPoint",
"UglyToad.PdfPig.Geometry.PdfLine",

View File

@@ -9502,75 +9502,111 @@ namespace UglyToad.PdfPig.Export
public enum PageXmlTextSimpleType
{
/// <remarks/>
/// <summary>
/// Paragraph
/// </summary>
[XmlEnumAttribute("paragraph")]
Paragraph,
/// <remarks/>
/// <summary>
/// Heading
/// </summary>
[XmlEnumAttribute("heading")]
Heading,
/// <remarks/>
/// <summary>
/// Caption
/// </summary>
[XmlEnumAttribute("caption")]
Caption,
/// <remarks/>
/// <summary>
/// Header
/// </summary>
[XmlEnumAttribute("header")]
Header,
/// <remarks/>
/// <summary>
/// Footer
/// </summary>
[XmlEnumAttribute("footer")]
Footer,
/// <remarks/>
/// <summary>
/// Page number
/// </summary>
[XmlEnumAttribute("page-number")]
PageNumber,
/// <remarks/>
/// <summary>
/// Drop Capital, a letter a the beginning of a word that is bigger than the usual character size. Usually to start a chapter.
/// </summary>
[XmlEnumAttribute("drop-capital")]
DropCapital,
/// <remarks/>
/// <summary>
/// Credit
/// </summary>
[XmlEnumAttribute("credit")]
Credit,
/// <remarks/>
/// <summary>
/// Floating
/// </summary>
[XmlEnumAttribute("floating")]
Floating,
/// <remarks/>
/// <summary>
/// Signature mark
/// </summary>
[XmlEnumAttribute("signature-mark")]
SignatureMark,
/// <remarks/>
/// <summary>
/// Catch word
/// </summary>
[XmlEnumAttribute("catch-word")]
CatchWord,
/// <remarks/>
/// <summary>
/// Marginalia
/// </summary>
[XmlEnumAttribute("marginalia")]
Marginalia,
/// <remarks/>
/// <summary>
/// Foot note
/// </summary>
[XmlEnumAttribute("footnote")]
FootNote,
/// <remarks/>
/// <summary>
/// Foot note - continued
/// </summary>
[XmlEnumAttribute("footnote-continued")]
FootNoteContinued,
/// <remarks/>
/// <summary>
/// End note
/// </summary>
[XmlEnumAttribute("endnote")]
EndNote,
/// <remarks/>
/// <summary>
/// Table of content
/// </summary>
[XmlEnumAttribute("TOC-entry")]
TocEntry,
/// <remarks/>
/// <summary>
/// List
/// </summary>
[XmlEnumAttribute("list-label")]
LisLabel,
/// <remarks/>
/// <summary>
/// Other
/// </summary>
[XmlEnumAttribute("other")]
Other,
}

View File

@@ -0,0 +1,518 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static UglyToad.PdfPig.Geometry.PdfPath;
namespace UglyToad.PdfPig.Geometry
{
/// <summary>
/// Extension class to Geometry.
/// </summary>
public static class GeometryExtensions
{
#region PdfRectangle
/// <summary>
/// Whether the rectangle contains the point.
/// </summary>
/// <param name="rectangle">The rectangle that should contain the point.</param>
/// <param name="point">The point that should be contained within the rectangle.</param>
/// <param name="includeBorder">If set to false, will return false if the point belongs to the border.</param>
public static bool Contains(this PdfRectangle rectangle, PdfPoint point, bool includeBorder = false)
{
if (includeBorder)
{
return point.X >= rectangle.Left &&
point.X <= rectangle.Right &&
point.Y >= rectangle.Bottom &&
point.Y <= rectangle.Top;
}
return point.X > rectangle.Left &&
point.X < rectangle.Right &&
point.Y > rectangle.Bottom &&
point.Y < rectangle.Top;
}
/// <summary>
/// Whether two rectangles overlap.
/// <para>Returns false if the two rectangles only share a border.</para>
/// </summary>
public static bool IntersectsWith(this PdfRectangle rectangle, PdfRectangle other)
{
if (rectangle.Left > other.Right || other.Left > rectangle.Right)
{
return false;
}
if (rectangle.Top < other.Bottom || other.Top < rectangle.Bottom)
{
return false;
}
return true;
}
/// <summary>
/// Get the <see cref="PdfRectangle"/> that is the intersection of two rectangles.
/// </summary>
public static PdfRectangle? Intersect(this PdfRectangle rectangle, PdfRectangle other)
{
if (!rectangle.IntersectsWith(other)) return null;
return new PdfRectangle(Math.Max(rectangle.BottomLeft.X, other.BottomLeft.X),
Math.Max(rectangle.BottomLeft.Y, other.BottomLeft.Y),
Math.Min(rectangle.TopRight.X, other.TopRight.X),
Math.Min(rectangle.TopRight.Y, other.TopRight.Y));
}
#endregion
#region PdfLine
/// <summary>
/// Whether the line segment contains the point.
/// </summary>
public static bool Contains(this PdfLine line, PdfPoint point)
{
if (line.Point2.X == line.Point1.X)
{
if (point.X == line.Point2.X)
{
return Math.Sign(point.Y - line.Point2.Y) != Math.Sign(point.Y - line.Point1.Y);
}
return false;
}
if (line.Point2.Y == line.Point1.Y)
{
if (point.Y == line.Point2.Y)
{
return Math.Sign(point.X - line.Point2.X) != Math.Sign(point.X - line.Point1.X);
}
return false;
}
var tx = (point.X - line.Point1.X) / (line.Point2.X - line.Point1.X);
var ty = (point.Y - line.Point1.Y) / (line.Point2.Y - line.Point1.Y);
if (Math.Round(tx - ty, 5) != 0) return false;
return (tx >= 0 && tx <= 1);
}
/// <summary>
/// Whether two lines intersect.
/// </summary>
public static bool IntersectsWith(this PdfLine line, PdfLine other)
{
return Intersect(line, other) != null;
}
/// <summary>
/// Get the <see cref="PdfPoint"/> that is the intersection of two lines.
/// </summary>
public static PdfPoint? Intersect(this PdfLine line, PdfLine other)
{
// if the bounding boxes do not intersect, the lines cannot intersect
if (!line.GetBoundingRectangle().IntersectsWith(other.GetBoundingRectangle()))
{
return null;
}
var eq1 = GetSlopeIntercept(line.Point1, line.Point2);
var eq2 = GetSlopeIntercept(other.Point1, other.Point2);
if (double.IsNaN(eq1.Slope) && double.IsNaN(eq2.Slope)) return null; // both lines are vertical (hence parallel)
if (eq1.Slope == eq2.Slope) return null; // both lines are parallel
var intersection = new PdfPoint();
if (double.IsNaN(eq1.Slope))
{
var x = eq1.Intercept;
var y = eq2.Slope * x + eq2.Intercept;
intersection = new PdfPoint(x, y);
}
else if (double.IsNaN(eq2.Slope))
{
var x = eq2.Intercept;
var y = eq1.Slope * x + eq1.Intercept;
intersection = new PdfPoint(x, y);
}
else
{
var x = (eq2.Intercept - eq1.Intercept) / (eq1.Slope - eq2.Slope);
var y = eq1.Slope * x + eq1.Intercept;
intersection = new PdfPoint(x, y);
}
// check if the intersection point belongs to both segments
// (for the moment we only know it belongs to both lines)
if (!line.Contains(intersection)) return null;
if (!other.Contains(intersection)) return null;
return intersection;
}
/// <summary>
/// Checks if both lines are parallel.
/// </summary>
public static bool ParallelTo(this PdfLine line, PdfLine other)
{
var val1 = (line.Point2.Y - line.Point1.Y) * (other.Point2.X - other.Point1.X);
var val2 = (other.Point2.Y - other.Point1.Y) * (line.Point2.X - line.Point1.X);
return Math.Round(val1 - val2, 5) == 0;
}
#endregion
#region Path Line
/// <summary>
/// Whether the line segment contains the point.
/// </summary>
public static bool Contains(this Line line, PdfPoint point)
{
if (line.To.X == line.From.X)
{
if (point.X == line.To.X)
{
return Math.Sign(point.Y - line.To.Y) != Math.Sign(point.Y - line.From.Y);
}
return false;
}
if (line.To.Y == line.From.Y)
{
if (point.Y == line.To.Y)
{
return Math.Sign(point.X - line.To.X) != Math.Sign(point.X - line.From.X);
}
return false;
}
var tx = (point.X - line.From.X) / (line.To.X - line.From.X);
var ty = (point.Y - line.From.Y) / (line.To.Y - line.From.Y);
if (Math.Round(tx - ty, 5) != 0) return false;
return (tx >= 0 && tx <= 1);
}
/// <summary>
/// Whether two lines intersect.
/// </summary>
public static bool IntersectsWith(this Line line, Line other)
{
return Intersect(line, other) != null;
}
/// <summary>
/// Get the <see cref="PdfPoint"/> that is the intersection of two lines.
/// </summary>
public static PdfPoint? Intersect(this Line line, Line other)
{
// if the bounding boxes do not intersect, the lines cannot intersect
var thisLineBbox = line.GetBoundingRectangle();
if (!thisLineBbox.HasValue) return null;
var lineBbox = other.GetBoundingRectangle();
if (!lineBbox.HasValue) return null;
if (!thisLineBbox.Value.IntersectsWith(lineBbox.Value))
{
return null;
}
var eq1 = GetSlopeIntercept(line.From, line.To);
var eq2 = GetSlopeIntercept(other.From, other.To);
if (double.IsNaN(eq1.Slope) && double.IsNaN(eq2.Slope)) return null; // both lines are vertical (hence parallel)
if (eq1.Slope == eq2.Slope) return null; // both lines are parallel
var intersection = new PdfPoint();
if (double.IsNaN(eq1.Slope))
{
var x = eq1.Intercept;
var y = eq2.Slope * x + eq2.Intercept;
intersection = new PdfPoint(x, y);
}
else if (double.IsNaN(eq2.Slope))
{
var x = eq2.Intercept;
var y = eq1.Slope * x + eq1.Intercept;
intersection = new PdfPoint(x, y);
}
else
{
var x = (eq2.Intercept - eq1.Intercept) / (eq1.Slope - eq2.Slope);
var y = eq1.Slope * x + eq1.Intercept;
intersection = new PdfPoint(x, y);
}
// check if the intersection point belongs to both segments
// (for the moment we only know it belongs to both lines)
if (!line.Contains(intersection)) return null;
if (!other.Contains(intersection)) return null;
return intersection;
}
/// <summary>
/// Checks if both lines are parallel.
/// </summary>
public static bool ParallelTo(this Line line, Line other)
{
var val1 = (line.To.Y - line.From.Y) * (other.To.X - other.From.X);
var val2 = (other.To.Y - other.From.Y) * (line.To.X - line.From.X);
return Math.Round(val1 - val2, 5) == 0;
}
#endregion
#region Path Bezier Curve
/// <summary>
/// Split a bezier curve into 2 bezier curves, at tau.
/// </summary>
/// <param name="bezierCurve">The original bezier curve.</param>
/// <param name="tau">The t value were to split the curve, usually between 0 and 1, but not necessary.</param>
private static (BezierCurve, BezierCurve) Split(this BezierCurve bezierCurve, decimal tau)
{
// De Casteljau Algorithm
PdfPoint[][] points = new PdfPoint[4][];
points[0] = new PdfPoint[]
{
bezierCurve.StartPoint,
bezierCurve.FirstControlPoint,
bezierCurve.SecondControlPoint,
bezierCurve.EndPoint
};
points[1] = new PdfPoint[3];
points[2] = new PdfPoint[2];
points[3] = new PdfPoint[1];
for (int j = 1; j <= 3; j++)
{
for (int i = 0; i <= 3 - j; i++)
{
var x = (1 - tau) * points[j - 1][i].X + tau * points[j - 1][i + 1].X;
var y = (1 - tau) * points[j - 1][i].Y + tau * points[j - 1][i + 1].Y;
points[j][i] = new PdfPoint(x, y);
}
}
return (new BezierCurve(points[0][0], points[1][0], points[2][0], points[3][0]),
new BezierCurve(points[3][0], points[2][1], points[1][2], points[0][3]));
}
/// <summary>
/// Get the <see cref="PdfPoint"/>s that are the intersections of the line and the curve.
/// </summary>
/// <returns></returns>
public static PdfPoint[] Intersect(this BezierCurve bezierCurve, PdfLine line)
{
var ts = FindIntersectionT(bezierCurve, line);
if (ts.Count() == 0) return null;
List<PdfPoint> points = new List<PdfPoint>();
foreach (var t in ts)
{
PdfPoint point = new PdfPoint(
BezierCurve.ValueWithT((double)bezierCurve.StartPoint.X,
(double)bezierCurve.FirstControlPoint.X,
(double)bezierCurve.SecondControlPoint.X,
(double)bezierCurve.EndPoint.X,
t),
BezierCurve.ValueWithT((double)bezierCurve.StartPoint.Y,
(double)bezierCurve.FirstControlPoint.Y,
(double)bezierCurve.SecondControlPoint.Y,
(double)bezierCurve.EndPoint.Y,
t));
points.Add(point);
}
return points.ToArray();
}
/// <summary>
/// Get the <see cref="PdfPoint"/>s that are the intersections of the line and the curve.
/// </summary>
/// <returns></returns>
public static PdfPoint[] Intersect(this BezierCurve bezierCurve, Line line)
{
var ts = FindIntersectionT(bezierCurve, line);
if (ts.Count() == 0) return null;
List<PdfPoint> points = new List<PdfPoint>();
foreach (var t in ts)
{
PdfPoint point = new PdfPoint(
BezierCurve.ValueWithT((double)bezierCurve.StartPoint.X,
(double)bezierCurve.FirstControlPoint.X,
(double)bezierCurve.SecondControlPoint.X,
(double)bezierCurve.EndPoint.X,
t),
BezierCurve.ValueWithT((double)bezierCurve.StartPoint.Y,
(double)bezierCurve.FirstControlPoint.Y,
(double)bezierCurve.SecondControlPoint.Y,
(double)bezierCurve.EndPoint.Y,
t)
);
points.Add(point);
}
return points.ToArray();
}
/// <summary>
/// Get the t values that are the intersections of the line and the curve.
/// </summary>
/// <returns>List of t values where the <see cref="BezierCurve"/> and the <see cref="PdfLine"/> intersect.</returns>
public static double[] FindIntersectionT(this BezierCurve bezierCurve, PdfLine line)
{
// if the bounding boxes do not intersect, they cannot intersect
var bezierBbox = bezierCurve.GetBoundingRectangle();
if (!bezierBbox.HasValue) return null;
var lineBbox = line.GetBoundingRectangle();
if (!bezierBbox.Value.IntersectsWith(lineBbox))
{
return null;
}
double x1 = (double)line.Point1.X;
double y1 = (double)line.Point1.Y;
double x2 = (double)line.Point2.X;
double y2 = (double)line.Point2.Y;
return FindIntersectionT(bezierCurve, x1, y1, x2, y2);
}
/// <summary>
/// Get the t values that are the intersections of the line and the curve.
/// </summary>
/// <returns>List of t values where the <see cref="BezierCurve"/> and the <see cref="Line"/> intersect.</returns>
public static double[] FindIntersectionT(this BezierCurve bezierCurve, Line line)
{
// if the bounding boxes do not intersect, they cannot intersect
var bezierBbox = bezierCurve.GetBoundingRectangle();
if (!bezierBbox.HasValue) return null;
var lineBbox = line.GetBoundingRectangle();
if (!lineBbox.HasValue) return null;
if (!bezierBbox.Value.IntersectsWith(lineBbox.Value))
{
return null;
}
double x1 = (double)line.From.X;
double y1 = (double)line.From.Y;
double x2 = (double)line.To.X;
double y2 = (double)line.To.Y;
return FindIntersectionT(bezierCurve, x1, y1, x2, y2);
}
private static double[] FindIntersectionT(BezierCurve bezierCurve, double x1, double y1, double x2, double y2)
{
double A = (y2 - y1);
double B = (x1 - x2);
double C = x1 * (y1 - y2) + y1 * (x2 - x1);
double alpha = (double)bezierCurve.StartPoint.X * A + (double)bezierCurve.StartPoint.Y * B;
double beta = 3.0 * ((double)bezierCurve.FirstControlPoint.X * A + (double)bezierCurve.FirstControlPoint.Y * B);
double gamma = 3.0 * ((double)bezierCurve.SecondControlPoint.X * A + (double)bezierCurve.SecondControlPoint.Y * B);
double delta = (double)bezierCurve.EndPoint.X * A + (double)bezierCurve.EndPoint.Y * B;
double a = (-alpha + beta - gamma + delta);
double b = (3 * alpha - 2 * beta + gamma);
double c = -3 * alpha + beta;
double d = alpha + C;
var solution = SolveCubicEquation(a, b, c, d);
return solution.Where(s => !double.IsNaN(s)).Where(s => s >= -double.Epsilon && s <= 1.0).OrderBy(s => s).ToArray();
}
#endregion
private static readonly double oneThird = 0.333333333333333333333;
private static (double Slope, double Intercept) GetSlopeIntercept(PdfPoint point1, PdfPoint point2)
{
if ((point1.X - point2.X) != 0) // vertical line special case
{
var slope = (double)((point2.Y - point1.Y) / (point2.X - point1.X));
var intercept = (double)point2.Y - slope * (double)point2.X;
return (slope, intercept);
}
else
{
return (double.NaN, (double)point1.X);
}
}
private static double CubicRoot(double d)
{
if (d < 0.0) return -Math.Pow(-d, oneThird);
return Math.Pow(d, oneThird);
}
/// <summary>
/// Get the real roots of a Cubic (or Quadratic, a=0) equation.
/// <para>ax^3 + bx^2 + cx + d = 0</para>
/// </summary>
/// <param name="a">ax^3</param>
/// <param name="b">bx^2</param>
/// <param name="c">cx</param>
/// <param name="d">d</param>
private static double[] SolveCubicEquation(double a, double b, double c, double d)
{
if (Math.Abs(a) <= double.Epsilon)
{
// handle Quadratic equation (a=0)
double detQ = c * c - 4 * b * d;
if (detQ >= 0)
{
double x = (-c + Math.Sqrt(detQ)) / (2.0 * b);
double x0 = (-c - Math.Sqrt(detQ)) / (2.0 * b);
return new double[] { x, x0 };
}
return new double[0]; // no real roots
}
double aSquared = a * a;
double aCubed = aSquared * a;
double bCubed = b * b * b;
double abc = a * b * c;
double bOver3a = b / (3.0 * a);
double Q = (3.0 * a * c - b * b) / (9.0 * aSquared);
double R = (9.0 * abc - 27.0 * aSquared * d - 2.0 * bCubed) / (54.0 * aCubed);
double det = Q * Q * Q + R * R; // same sign as determinant because: 4p^3 + 27q^2 = (4 * 27) * (Q^3 + R^2)
double x1 = double.NaN;
double x2 = double.NaN;
double x3 = double.NaN;
if (det >= 0) // Cardano's Formula
{
double sqrtDet = Math.Sqrt(det);
double S = CubicRoot(R + sqrtDet);
double T = CubicRoot(R - sqrtDet);
double SPlusT = S + T;
x1 = SPlusT - bOver3a; // real root
// Complex roots
double complexPart = Math.Sqrt(3) / 2.0 * (S - T); // complex part of complex root
if (Math.Abs(complexPart) <= double.Epsilon) // if complex part == 0
{
// complex roots only have real part
// the real part is the same for both roots
x2 = -SPlusT / 2 - bOver3a;
}
}
else // Casus irreducibilis
{
// François Viète's formula
Func<double, double, double, double> vietTrigonometricSolution = (p_, q_, k) => 2.0 * Math.Sqrt(-p_ / 3.0)
* Math.Cos(oneThird * Math.Acos((3.0 * q_) / (2.0 * p_) * Math.Sqrt(-3.0 / p_)) - (2.0 * Math.PI * k) / 3.0);
double p = Q * 3.0; // (3.0 * a * c - b * b) / (3.0 * aSquared);
double q = -R * 2.0; // (2.0 * bCubed - 9.0 * abc + 27.0 * aSquared * d) / (27.0 * aCubed);
x1 = vietTrigonometricSolution(p, q, 0) - bOver3a;
x2 = vietTrigonometricSolution(p, q, 1) - bOver3a;
x3 = vietTrigonometricSolution(p, q, 2) - bOver3a;
}
return new double[] { x1, x2, x3 };
}
}
}

View File

@@ -1,4 +1,6 @@
namespace UglyToad.PdfPig.Geometry
using System;
namespace UglyToad.PdfPig.Geometry
{
/// <summary>
/// A line in a PDF file.
@@ -53,6 +55,18 @@
Point2 = point2;
}
/// <summary>
/// The rectangle completely containing the <see cref="PdfLine"/>.
/// </summary>
public PdfRectangle GetBoundingRectangle()
{
return new PdfRectangle(
Math.Min(this.Point1.X, this.Point2.X),
Math.Min(this.Point1.Y, this.Point2.Y),
Math.Max(this.Point1.X, this.Point2.X),
Math.Max(this.Point1.Y, this.Point2.Y));
}
/// <summary>
/// Returns a value indicating whether this <see cref="PdfLine"/> is equal to a specified <see cref="PdfLine"/> .
/// </summary>

View File

@@ -18,6 +18,11 @@ namespace UglyToad.PdfPig.Geometry
/// </summary>
public IReadOnlyList<IPathCommand> Commands => commands;
/// <summary>
/// True if the <see cref="PdfPath"/> was originaly draw as a rectangle.
/// </summary>
public bool IsDrawnAsRectangle { get; internal set; }
private PdfPoint? currentPosition;
private double shoeLaceSum;
@@ -118,8 +123,8 @@ namespace UglyToad.PdfPig.Geometry
/// <summary>
/// Simplify this <see cref="PdfPath"/> by converting everything to <see cref="PdfLine"/>s.
/// </summary>
/// <returns></returns>
internal PdfPath Simplify()
/// <param name="n">Number of lines required (minimum is 1).</param>
internal PdfPath Simplify(int n = 4)
{
PdfPath simplifiedPath = new PdfPath();
var startPoint = GetStartPoint(Commands.First());
@@ -133,7 +138,7 @@ namespace UglyToad.PdfPig.Geometry
}
else if (command is BezierCurve curve)
{
foreach (var lineB in curve.ToLines(4))
foreach (var lineB in curve.ToLines(n))
{
simplifiedPath.LineTo(lineB.To.X, lineB.To.Y);
}
@@ -351,6 +356,26 @@ namespace UglyToad.PdfPig.Geometry
{
builder.Append("Z ");
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
return (obj is Close);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return base.GetHashCode();
}
}
/// <summary>
@@ -385,6 +410,22 @@ namespace UglyToad.PdfPig.Geometry
{
builder.Append("M ").Append(Location.X).Append(' ').Append(Location.Y).Append(' ');
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is Move move)
{
return this.Location.Equals(move.Location);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (this.Location).GetHashCode();
}
}
/// <summary>
@@ -422,6 +463,22 @@ namespace UglyToad.PdfPig.Geometry
{
builder.AppendFormat("L {0} {1} ", To.X, To.Y);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is Line line)
{
return this.From.Equals(line.From) && this.To.Equals(line.To);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (this.From, this.To).GetHashCode();
}
}
/// <summary>
@@ -592,7 +649,7 @@ namespace UglyToad.PdfPig.Geometry
return true;
}
private static double ValueWithT(double p1, double p2, double p3, double p4, double t)
internal static double ValueWithT(double p1, double p2, double p3, double p4, double t)
{
// P = (1t)^3*P_1 + 3(1t)^2*t*P_2 + 3(1t)*t^2*P_3 + t^3*P_4
var oneMinusT = 1 - t;
@@ -629,6 +686,25 @@ namespace UglyToad.PdfPig.Geometry
}
return lines;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is BezierCurve curve)
{
return this.StartPoint.Equals(curve.StartPoint) &&
this.FirstControlPoint.Equals(curve.FirstControlPoint) &&
this.SecondControlPoint.Equals(curve.SecondControlPoint) &&
this.EndPoint.Equals(curve.EndPoint);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (this.StartPoint, this.FirstControlPoint, this.SecondControlPoint, this.EndPoint).GetHashCode();
}
}
internal void Rectangle(decimal x, decimal y, decimal width, decimal height)
@@ -638,6 +714,38 @@ namespace UglyToad.PdfPig.Geometry
LineTo(x + width, y + height);
LineTo(x, y + height);
LineTo(x, y);
IsDrawnAsRectangle = true;
}
/// <summary>
/// Compares two <see cref="PdfPath"/>s for equality. Paths will only be considered equal if the commands which construct the paths are in the same order.
/// </summary>
public override bool Equals(object obj)
{
if (obj is PdfPath path)
{
if (this.Commands.Count != path.Commands.Count) return false;
for (int i = 0; i < this.Commands.Count; i++)
{
if (!this.Commands[i].Equals(path.Commands[i])) return false;
}
return true;
}
return false;
}
/// <summary>
/// Get the hash code. Paths will only have the same hash code if the commands which construct the paths are in the same order.
/// </summary>
public override int GetHashCode()
{
var hash = this.Commands.Count + 1;
for (int i = 0; i < this.Commands.Count; i++)
{
hash = hash * (i + 1) * 17 + this.Commands[i].GetHashCode();
}
return hash;
}
}
}

View File

@@ -77,6 +77,17 @@
return new PdfPoint(X, Y + dy);
}
/// <summary>
/// Creates a new <see cref="PdfPoint"/> which is the current point moved in the x and y directions relative to its current position by a value.
/// </summary>
/// <param name="dx">The distance to move the point in the x direction relative to its current location.</param>
/// <param name="dy">The distance to move the point in the y direction relative to its current location.</param>
/// <returns>A new point shifted on the y axis by the given delta value.</returns>
public PdfPoint Translate(decimal dx, decimal dy)
{
return new PdfPoint(X + dx, Y + dy);
}
internal PdfVector ToVector()
{
return new PdfVector(X, Y);

View File

@@ -126,23 +126,15 @@
BottomRight = bottomRight;
}
/// <summary>
/// Whether two rectangles overlap.
/// Creates a new <see cref="PdfRectangle"/> which is the current rectangle moved in the x and y directions relative to its current position by a value.
/// </summary>
public bool IntersectsWith(PdfRectangle rectangle)
/// <param name="dx">The distance to move the rectangle in the x direction relative to its current location.</param>
/// <param name="dy">The distance to move the rectangle in the y direction relative to its current location.</param>
/// <returns>A new rectangle shifted on the y axis by the given delta value.</returns>
public PdfRectangle Translate(decimal dx, decimal dy)
{
if (Left > rectangle.Right || rectangle.Left > Right)
{
return false;
}
if (Top < rectangle.Bottom || rectangle.Top < Bottom)
{
return false;
}
return true;
return new PdfRectangle(this.BottomLeft.Translate(dx, dy), this.TopRight.Translate(dx, dy));
}
/// <summary>