From 1656411fcb0d5100ae763d94d9e87dae18d35f8f Mon Sep 17 00:00:00 2001 From: BobLd Date: Sat, 14 Dec 2019 11:41:11 +0000 Subject: [PATCH 1/2] Improving Geometry classes with Tests --- .../Geometry/PdfLineTests.cs | 173 ++++++ .../Geometry/PdfRectangleTests.cs | 57 ++ .../PublicApiScannerTests.cs | 1 + .../Export/PageXmlTextExporter.cs | 72 ++- .../Geometry/GeometryExtensions.cs | 508 ++++++++++++++++++ src/UglyToad.PdfPig/Geometry/PdfLine.cs | 16 +- src/UglyToad.PdfPig/Geometry/PdfPath.cs | 116 +++- src/UglyToad.PdfPig/Geometry/PdfPoint.cs | 11 + src/UglyToad.PdfPig/Geometry/PdfRectangle.cs | 20 +- 9 files changed, 937 insertions(+), 37 deletions(-) create mode 100644 src/UglyToad.PdfPig.Tests/Geometry/PdfLineTests.cs create mode 100644 src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs create mode 100644 src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs diff --git a/src/UglyToad.PdfPig.Tests/Geometry/PdfLineTests.cs b/src/UglyToad.PdfPig.Tests/Geometry/PdfLineTests.cs new file mode 100644 index 00000000..95be2b8d --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Geometry/PdfLineTests.cs @@ -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)); + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs b/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs new file mode 100644 index 00000000..aab5eb02 --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs @@ -0,0 +1,57 @@ +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 + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs index e5d99ac9..84ff6e61 100644 --- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs +++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs @@ -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", diff --git a/src/UglyToad.PdfPig/Export/PageXmlTextExporter.cs b/src/UglyToad.PdfPig/Export/PageXmlTextExporter.cs index 1e4f66f4..f2d7fa89 100644 --- a/src/UglyToad.PdfPig/Export/PageXmlTextExporter.cs +++ b/src/UglyToad.PdfPig/Export/PageXmlTextExporter.cs @@ -9502,75 +9502,111 @@ namespace UglyToad.PdfPig.Export public enum PageXmlTextSimpleType { - /// + /// + /// Paragraph + /// [XmlEnumAttribute("paragraph")] Paragraph, - /// + /// + /// Heading + /// [XmlEnumAttribute("heading")] Heading, - /// + /// + /// Caption + /// [XmlEnumAttribute("caption")] Caption, - /// + /// + /// Header + /// [XmlEnumAttribute("header")] Header, - /// + /// + /// Footer + /// [XmlEnumAttribute("footer")] Footer, - /// + /// + /// Page number + /// [XmlEnumAttribute("page-number")] PageNumber, - /// + /// + /// Drop Capital, a letter a the beginning of a word that is bigger than the usual character size. Usually to start a chapter. + /// [XmlEnumAttribute("drop-capital")] DropCapital, - /// + /// + /// Credit + /// [XmlEnumAttribute("credit")] Credit, - /// + /// + /// Floating + /// [XmlEnumAttribute("floating")] Floating, - /// + /// + /// Signature mark + /// [XmlEnumAttribute("signature-mark")] SignatureMark, - /// + /// + /// Catch word + /// [XmlEnumAttribute("catch-word")] CatchWord, - /// + /// + /// Marginalia + /// [XmlEnumAttribute("marginalia")] Marginalia, - /// + /// + /// Foot note + /// [XmlEnumAttribute("footnote")] FootNote, - /// + /// + /// Foot note - continued + /// [XmlEnumAttribute("footnote-continued")] FootNoteContinued, - /// + /// + /// End note + /// [XmlEnumAttribute("endnote")] EndNote, - /// + /// + /// Table of content + /// [XmlEnumAttribute("TOC-entry")] TocEntry, - /// + /// + /// List + /// [XmlEnumAttribute("list-label")] LisLabel, - /// + /// + /// Other + /// [XmlEnumAttribute("other")] Other, } diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs new file mode 100644 index 00000000..a3b7db49 --- /dev/null +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static UglyToad.PdfPig.Geometry.PdfPath; + +namespace UglyToad.PdfPig.Geometry +{ + /// + /// Extension class to Geometry. + /// + public static class GeometryExtensions + { + #region PdfRectangle + /// + /// Whether the rectangle contains the point. + /// Return false if the point belongs to the border. + /// + public static bool Contains(this PdfRectangle rectangle, PdfPoint point) + { + return point.X > rectangle.Left && + point.X < rectangle.Right && + point.Y > rectangle.Bottom && + point.Y < rectangle.Top; + } + + /// + /// Whether two rectangles overlap. + /// Returns false if the two rectangles only share a border. + /// + 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; + } + + /// + /// Get the that is the intersection of two rectangles. + /// + 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 + /// + /// Whether the line segment contains the point. + /// + 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); + } + + /// + /// Whether two lines intersect. + /// + public static bool IntersectsWith(this PdfLine line, PdfLine other) + { + return Intersect(line, other) != null; + } + + /// + /// Get the that is the intersection of two lines. + /// + 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; + } + + /// + /// Checks if both lines are parallel. + /// + 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 + /// + /// Whether the line segment contains the point. + /// + 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); + } + + /// + /// Whether two lines intersect. + /// + public static bool IntersectsWith(this Line line, Line other) + { + return Intersect(line, other) != null; + } + + /// + /// Get the that is the intersection of two lines. + /// + 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; + } + + /// + /// Checks if both lines are parallel. + /// + 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 + /// + /// Split a bezier curve into 2 bezier curves, at tau. + /// + /// The original bezier curve. + /// The t value were to split the curve, usually between 0 and 1, but not necessary. + 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])); + } + + /// + /// Get the s that are the intersections of the line and the curve. + /// + /// + public static PdfPoint[] Intersect(this BezierCurve bezierCurve, PdfLine line) + { + var ts = FindIntersectionT(bezierCurve, line); + if (ts.Count() == 0) return null; + + List points = new List(); + 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(); + } + + /// + /// Get the s that are the intersections of the line and the curve. + /// + /// + public static PdfPoint[] Intersect(this BezierCurve bezierCurve, Line line) + { + var ts = FindIntersectionT(bezierCurve, line); + if (ts.Count() == 0) return null; + + List points = new List(); + 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(); + } + + /// + /// Get the t values that are the intersections of the line and the curve. + /// + /// List of t values where the and the intersect. + 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); + } + + /// + /// Get the t values that are the intersections of the line and the curve. + /// + /// List of t values where the and the intersect. + 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); + } + + /// + /// Get the real roots of a Cubic (or Quadratic, a=0) equation. + /// ax^3 + bx^2 + cx + d = 0 + /// + /// ax^3 + /// bx^2 + /// cx + /// d + 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 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 }; + } + } +} diff --git a/src/UglyToad.PdfPig/Geometry/PdfLine.cs b/src/UglyToad.PdfPig/Geometry/PdfLine.cs index dc281f0b..95f7fbcc 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfLine.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfLine.cs @@ -1,4 +1,6 @@ -namespace UglyToad.PdfPig.Geometry +using System; + +namespace UglyToad.PdfPig.Geometry { /// /// A line in a PDF file. @@ -53,6 +55,18 @@ Point2 = point2; } + /// + /// The rectangle completely containing the . + /// + 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)); + } + /// /// Returns a value indicating whether this is equal to a specified . /// diff --git a/src/UglyToad.PdfPig/Geometry/PdfPath.cs b/src/UglyToad.PdfPig/Geometry/PdfPath.cs index a01a31f2..97c00b16 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfPath.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfPath.cs @@ -18,6 +18,11 @@ namespace UglyToad.PdfPig.Geometry /// public IReadOnlyList Commands => commands; + /// + /// True if the was originaly draw as a rectangle. + /// + internal bool IsDrawnAsRectangle { get; set; } + private PdfPoint? currentPosition; private double shoeLaceSum; @@ -118,8 +123,8 @@ namespace UglyToad.PdfPig.Geometry /// /// Simplify this by converting everything to s. /// - /// - internal PdfPath Simplify() + /// Number of lines required (minimum is 1). + 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 "); } + + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return (obj is Close); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } } /// @@ -385,6 +410,22 @@ namespace UglyToad.PdfPig.Geometry { builder.Append("M ").Append(Location.X).Append(' ').Append(Location.Y).Append(' '); } + + /// + public override bool Equals(object obj) + { + if (obj is Move move) + { + return this.Location.Equals(move.Location); + } + return false; + } + + /// + public override int GetHashCode() + { + return (this.Location).GetHashCode(); + } } /// @@ -422,6 +463,22 @@ namespace UglyToad.PdfPig.Geometry { builder.AppendFormat("L {0} {1} ", To.X, To.Y); } + + /// + public override bool Equals(object obj) + { + if (obj is Line line) + { + return this.From.Equals(line.From) && this.To.Equals(line.To); + } + return false; + } + + /// + public override int GetHashCode() + { + return (this.From, this.To).GetHashCode(); + } } /// @@ -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 = (1−t)^3*P_1 + 3(1−t)^2*t*P_2 + 3(1−t)*t^2*P_3 + t^3*P_4 var oneMinusT = 1 - t; @@ -629,6 +686,25 @@ namespace UglyToad.PdfPig.Geometry } return lines; } + + /// + 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; + } + + /// + 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; + } + + /// + /// Order matters + /// + 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; + } + + /// + /// Order matters + /// + public override int GetHashCode() + { + var hash = this.Commands.Count + 1; + for (int i = 0; i < this.Commands.Count; i++) + { + hash = hash * (i + 1) + this.Commands[i].GetHashCode(); + } + return hash; } } } diff --git a/src/UglyToad.PdfPig/Geometry/PdfPoint.cs b/src/UglyToad.PdfPig/Geometry/PdfPoint.cs index 8ddc1bdf..d558e2b1 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfPoint.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfPoint.cs @@ -77,6 +77,17 @@ return new PdfPoint(X, Y + dy); } + /// + /// Creates a new which is the current point moved in the x and y directions relative to its current position by a value. + /// + /// The distance to move the point in the x direction relative to its current location. + /// The distance to move the point in the y direction relative to its current location. + /// A new point shifted on the y axis by the given delta value. + public PdfPoint MoveXY(decimal dx, decimal dy) + { + return new PdfPoint(X + dx, Y + dy); + } + internal PdfVector ToVector() { return new PdfVector(X, Y); diff --git a/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs b/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs index d9290c1f..09c23da4 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs @@ -126,23 +126,15 @@ BottomRight = bottomRight; } - /// - /// Whether two rectangles overlap. + /// Creates a new which is the current rectangle moved in the x and y directions relative to its current position by a value. /// - public bool IntersectsWith(PdfRectangle rectangle) + /// The distance to move the rectangle in the x direction relative to its current location. + /// The distance to move the rectangle in the y direction relative to its current location. + /// A new rectangle shifted on the y axis by the given delta value. + public PdfRectangle MoveXY(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.MoveXY(dx, dy), this.TopRight.MoveXY(dx, dy)); } /// From 5cf1f6c58c6a3ac9966d87ed543ae2b2e343bf40 Mon Sep 17 00:00:00 2001 From: BobLd Date: Mon, 16 Dec 2019 14:36:52 +0000 Subject: [PATCH 2/2] Modifications and adding som tests --- .../Geometry/PdfRectangleTests.cs | 9 +++++++++ src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs | 14 ++++++++++++-- src/UglyToad.PdfPig/Geometry/PdfPath.cs | 8 ++++---- src/UglyToad.PdfPig/Geometry/PdfPoint.cs | 2 +- src/UglyToad.PdfPig/Geometry/PdfRectangle.cs | 4 ++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs b/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs index aab5eb02..367df001 100644 --- a/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs +++ b/src/UglyToad.PdfPig.Tests/Geometry/PdfRectangleTests.cs @@ -53,5 +53,14 @@ namespace UglyToad.PdfPig.Tests.Geometry 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)); + } } } diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index a3b7db49..155fae28 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -13,10 +13,20 @@ namespace UglyToad.PdfPig.Geometry #region PdfRectangle /// /// Whether the rectangle contains the point. - /// Return false if the point belongs to the border. /// - public static bool Contains(this PdfRectangle rectangle, PdfPoint point) + /// The rectangle that should contain the point. + /// The point that should be contained within the rectangle. + /// If set to false, will return false if the point belongs to the border. + 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 && diff --git a/src/UglyToad.PdfPig/Geometry/PdfPath.cs b/src/UglyToad.PdfPig/Geometry/PdfPath.cs index 97c00b16..71bc8e8f 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfPath.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfPath.cs @@ -21,7 +21,7 @@ namespace UglyToad.PdfPig.Geometry /// /// True if the was originaly draw as a rectangle. /// - internal bool IsDrawnAsRectangle { get; set; } + public bool IsDrawnAsRectangle { get; internal set; } private PdfPoint? currentPosition; @@ -718,7 +718,7 @@ namespace UglyToad.PdfPig.Geometry } /// - /// Order matters + /// Compares two s for equality. Paths will only be considered equal if the commands which construct the paths are in the same order. /// public override bool Equals(object obj) { @@ -736,14 +736,14 @@ namespace UglyToad.PdfPig.Geometry } /// - /// Order matters + /// Get the hash code. Paths will only have the same hash code if the commands which construct the paths are in the same order. /// public override int GetHashCode() { var hash = this.Commands.Count + 1; for (int i = 0; i < this.Commands.Count; i++) { - hash = hash * (i + 1) + this.Commands[i].GetHashCode(); + hash = hash * (i + 1) * 17 + this.Commands[i].GetHashCode(); } return hash; } diff --git a/src/UglyToad.PdfPig/Geometry/PdfPoint.cs b/src/UglyToad.PdfPig/Geometry/PdfPoint.cs index d558e2b1..8fd13aed 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfPoint.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfPoint.cs @@ -83,7 +83,7 @@ /// The distance to move the point in the x direction relative to its current location. /// The distance to move the point in the y direction relative to its current location. /// A new point shifted on the y axis by the given delta value. - public PdfPoint MoveXY(decimal dx, decimal dy) + public PdfPoint Translate(decimal dx, decimal dy) { return new PdfPoint(X + dx, Y + dy); } diff --git a/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs b/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs index 09c23da4..5ccc8430 100644 --- a/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs +++ b/src/UglyToad.PdfPig/Geometry/PdfRectangle.cs @@ -132,9 +132,9 @@ /// The distance to move the rectangle in the x direction relative to its current location. /// The distance to move the rectangle in the y direction relative to its current location. /// A new rectangle shifted on the y axis by the given delta value. - public PdfRectangle MoveXY(decimal dx, decimal dy) + public PdfRectangle Translate(decimal dx, decimal dy) { - return new PdfRectangle(this.BottomLeft.MoveXY(dx, dy), this.TopRight.MoveXY(dx, dy)); + return new PdfRectangle(this.BottomLeft.Translate(dx, dy), this.TopRight.Translate(dx, dy)); } ///