From b9419469ac92e04521c303eefb9872bddcfe8373 Mon Sep 17 00:00:00 2001 From: BobLd Date: Sat, 29 Aug 2020 17:08:02 +0100 Subject: [PATCH 1/6] add PdfSubpath and PdfPath geometry extensions --- src/UglyToad.PdfPig.Core/PdfSubpath.cs | 4 +- .../Geometry/ClippingExtensions.cs | 27 +- .../Geometry/GeometryExtensions.cs | 246 ++++++++++++++++++ 3 files changed, 268 insertions(+), 9 deletions(-) diff --git a/src/UglyToad.PdfPig.Core/PdfSubpath.cs b/src/UglyToad.PdfPig.Core/PdfSubpath.cs index 32841be5..82fbe5b8 100644 --- a/src/UglyToad.PdfPig.Core/PdfSubpath.cs +++ b/src/UglyToad.PdfPig.Core/PdfSubpath.cs @@ -18,7 +18,8 @@ public IReadOnlyList Commands => commands; /// - /// True if the was originaly draw as a rectangle. + /// True if the was originaly drawn using the rectangle ('re') operator. + /// Always false if paths are clipped. /// public bool IsDrawnAsRectangle { get; internal set; } @@ -34,7 +35,6 @@ /// /// Return true if points are organised in a counterclockwise order. Works only with closed paths. /// - /// public bool IsCounterClockwise => IsClosed() && shoeLaceSum < 0; /// diff --git a/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs b/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs index 7e8221db..12d839ad 100644 --- a/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/ClippingExtensions.cs @@ -157,7 +157,7 @@ /// Converts a path to a set of points for the Clipper algorithm to use. /// Allows duplicate points as they will be removed by Clipper. /// - private static IEnumerable ToClipperPolygon(this PdfSubpath pdfPath) + internal static IEnumerable ToClipperPolygon(this PdfSubpath pdfPath) { if (pdfPath.Commands.Count == 0) { @@ -166,7 +166,7 @@ if (pdfPath.Commands[0] is Move currentMove) { - var previous = new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor); + var previous = currentMove.Location.ToClipperIntPoint(); yield return previous; @@ -190,22 +190,35 @@ if (command is Line line) { - yield return new ClipperIntPoint(line.From.X * Factor, line.From.Y * Factor); - yield return new ClipperIntPoint(line.To.X * Factor, line.To.Y * Factor); + yield return line.From.ToClipperIntPoint(); + yield return line.To.ToClipperIntPoint(); } else if (command is BezierCurve curve) { foreach (var lineB in curve.ToLines(LinesInCurve)) { - yield return new ClipperIntPoint(lineB.From.X * Factor, lineB.From.Y * Factor); - yield return new ClipperIntPoint(lineB.To.X * Factor, lineB.To.Y * Factor); + yield return lineB.From.ToClipperIntPoint(); + yield return lineB.To.ToClipperIntPoint(); } } else if (command is Close) { - yield return new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor); + yield return currentMove.Location.ToClipperIntPoint(); } } } + + internal static IEnumerable ToClipperPolygon(this PdfRectangle rectangle) + { + yield return rectangle.BottomLeft.ToClipperIntPoint(); + yield return rectangle.TopLeft.ToClipperIntPoint(); + yield return rectangle.TopRight.ToClipperIntPoint(); + yield return rectangle.BottomRight.ToClipperIntPoint(); + } + + internal static ClipperIntPoint ToClipperIntPoint(this PdfPoint point) + { + return new ClipperIntPoint(point.X * Factor, point.Y * Factor); + } } } diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index 1538d9f6..42c081a7 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; + using UglyToad.PdfPig.Geometry.ClipperLibrary; + using UglyToad.PdfPig.Graphics; using static UglyToad.PdfPig.Core.PdfSubpath; /// @@ -454,6 +456,7 @@ /// /// Gets the that is the intersection of two rectangles. + /// Only works for axis-aligned rectangles. /// public static PdfRectangle? Intersect(this PdfRectangle rectangle, PdfRectangle other) { @@ -815,6 +818,249 @@ } #endregion + #region PdfPath & PdfSubpath + #region Clipper extension + // https://stackoverflow.com/questions/54723622/point-in-polygon-hit-test-algorithm + // Ported from Angus Johnson's Delphi Pascal code (Clipper's author) + // Might be made available in the next Clipper release? + + private static double CrossProduct(ClipperIntPoint pt1, ClipperIntPoint pt2, ClipperIntPoint pt3) + { + return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - (pt2.Y - pt1.Y) * (pt3.X - pt2.X); + } + + private static int PointInPathsWindingCount(ClipperIntPoint pt, List> paths) + { + int i, j, len; + List p; + ClipperIntPoint prevPt; + bool isAbove; + double crossProd; + + //nb: returns MaxInt ((2^32)-1) when pt is on a line + + var Result = 0; // /!\ + for (i = 0; i < paths.Count; i++) + { + j = 0; + p = paths[i]; + len = p.Count; + if (len < 3) continue; + prevPt = p[len - 1]; + while ((j < len) && (p[j].Y == prevPt.Y)) j++; + if (j == len) continue; + isAbove = (prevPt.Y < pt.Y); + while (j < len) + { + if (isAbove) + { + while ((j < len) && (p[j].Y < pt.Y)) j++; + if (j == len) + { + break; + } + else if (j > 0) + { + prevPt = p[j - 1]; + } + crossProd = CrossProduct(prevPt, p[j], pt); + if (crossProd == 0) + { + return int.MaxValue; + //result:= MaxInt; + //Exit; + } + else if (crossProd < 0) + { + Result--; + } + } + else + { + while ((j < len) && (p[j].Y > pt.Y)) j++; + if (j == len) + { + break; + } + else if (j > 0) + { + prevPt = p[j - 1]; + } + crossProd = CrossProduct(prevPt, p[j], pt); + if (crossProd == 0) + { + return int.MaxValue; + //result:= MaxInt; + //Exit; + } + else if (crossProd > 0) + { + Result++; + } + } + j++; + isAbove = !isAbove; + } + } + return Result; + } + + private static bool PointInPaths(ClipperIntPoint pt, List> paths, ClipperPolyFillType fillRule, bool includeBorder) + { + int wc = PointInPathsWindingCount(pt, paths); + if (wc == int.MaxValue) + { + return includeBorder; + } + + switch (fillRule) + { + case ClipperPolyFillType.EvenOdd: + return wc % 2 != 0; // Odd() + + case ClipperPolyFillType.NonZero: + default: + return wc != 0; + } + } + #endregion + + /// + /// Whether the subpath contains the point. + /// Ignores winding rule. + /// + /// The subpath that should contain the point. + /// The point that should be contained within the subpath. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfPoint point, bool includeBorder = false) + { + return PointInPaths(point.ToClipperIntPoint(), + new List>() { subpath.ToClipperPolygon().ToList() }, + ClipperPolyFillType.EvenOdd, + includeBorder); + } + + /// + /// Whether the subpath contains the rectangle. + /// Ignores winding rule. + /// + /// The subpath that should contain the rectangle. + /// The rectangle that should be contained within the subpath. + /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfRectangle rectangle, bool includeBorder = false) + { + var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; + if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + return true; + } + + /// + /// Whether the subpath contains the other subpath. + /// Ignores winding rule. + /// + /// The subpath that should contain the rectangle. + /// The other subpath that should be contained within the subpath. + /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfSubpath subpath, PdfSubpath other, bool includeBorder = false) + { + var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; + foreach (var pt in other.ToClipperPolygon()) + { + if (!PointInPaths(pt, clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + } + return true; + } + + /// + /// Get the area of the path. + /// + /// + public static double GetArea(this PdfPath path) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var simplifieds = Clipper.SimplifyPolygons(clipperPaths, path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd); + double sum = 0; + foreach (var simplified in simplifieds) + { + sum += Clipper.Area(simplified); + } + return sum; + } + + /// + /// Whether the path contains the point. + /// + /// The path that should contain the point. + /// The point that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfPoint point, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + return PointInPaths(point.ToClipperIntPoint(), + clipperPaths, + path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd, + includeBorder); + } + + /// + /// Whether the path contains the rectangle. + /// + /// The path that should contain the rectangle. + /// The rectangle that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfRectangle rectangle, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + return true; + } + + /// + /// Whether the path contains the subpath. + /// + /// The path that should contain the subpath. + /// The subpath that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfSubpath subpath, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + foreach (var p in subpath.ToClipperPolygon()) + { + if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false; + } + return true; + } + + /// + /// Whether the path contains the other path. + /// + /// The path that should contain the path. + /// The other path that should be contained within the path. + /// If set to false, will return false if the point belongs to the border. + public static bool Contains(this PdfPath path, PdfPath other, bool includeBorder = false) + { + var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); + var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; + foreach (var subpath in other) + { + foreach (var p in subpath.ToClipperPolygon()) + { + if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false; + } + } + return true; + } + + #endregion + private const double OneThird = 0.333333333333333333333; private const double SqrtOfThree = 1.73205080756888; From f656922d2d07e7a54f3841b2f016af631c2113de Mon Sep 17 00:00:00 2001 From: BobLd Date: Sat, 29 Aug 2020 19:27:12 +0100 Subject: [PATCH 2/6] add tests for PdfPath.Contains(Rectangle) - EvenOdd --- .../Geometry/PdfPathExtensionsTests.cs | 38 ++++++++++++++++++ .../Documents/path_ext_oddeven.pdf | Bin 0 -> 41967 bytes 2 files changed, 38 insertions(+) create mode 100644 src/UglyToad.PdfPig.Tests/Geometry/PdfPathExtensionsTests.cs create mode 100644 src/UglyToad.PdfPig.Tests/Integration/Documents/path_ext_oddeven.pdf diff --git a/src/UglyToad.PdfPig.Tests/Geometry/PdfPathExtensionsTests.cs b/src/UglyToad.PdfPig.Tests/Geometry/PdfPathExtensionsTests.cs new file mode 100644 index 00000000..2f27ec3d --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Geometry/PdfPathExtensionsTests.cs @@ -0,0 +1,38 @@ +namespace UglyToad.PdfPig.Tests.Geometry +{ + using System.Linq; + using UglyToad.PdfPig.Core; + using UglyToad.PdfPig.Geometry; + using UglyToad.PdfPig.Tests.Integration; + using Xunit; + + public class PdfPathExtensionsTests + { + [Fact] + public void ContainsRectangleEvenOdd() + { + using (var document = PdfDocument.Open(IntegrationHelpers.GetDocumentPath("path_ext_oddeven"), + new ParsingOptions() { ClipPaths = true })) + { + var page = document.GetPage(1); + var words = page.GetWords().ToList(); + + foreach (var path in page.ExperimentalAccess.Paths) + { + Assert.NotEqual(FillingRule.NonZeroWinding, path.FillingRule); // allow none and even-odd + + foreach (var c in words.Where(w => path.Contains(w.BoundingBox)).ToList()) + { + Assert.Equal("in", c.Text.Split("_").Last()); + words.Remove(c); + } + } + + foreach (var w in words) + { + Assert.NotEqual("in", w.Text.Split("_").Last()); + } + } + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/path_ext_oddeven.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/path_ext_oddeven.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2947742c3a0599cfda317e83dc0c28d2cf2b26fe GIT binary patch literal 41967 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 literal 0 HcmV?d00001 From ce3edcd0cc92baff964ebf1c591caf6f5c8f88e1 Mon Sep 17 00:00:00 2001 From: BobLd Date: Sun, 30 Aug 2020 13:44:35 +0100 Subject: [PATCH 3/6] tidy up code and use EvenOdd as default in PointInPaths() --- .../Geometry/GeometryExtensions.cs | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index 42c081a7..8aad608e 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -829,27 +829,26 @@ return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - (pt2.Y - pt1.Y) * (pt3.X - pt2.X); } + /// + /// nb: returns MaxInt ((2^32)-1) when pt is on a line + /// private static int PointInPathsWindingCount(ClipperIntPoint pt, List> paths) { - int i, j, len; - List p; - ClipperIntPoint prevPt; - bool isAbove; - double crossProd; - - //nb: returns MaxInt ((2^32)-1) when pt is on a line - - var Result = 0; // /!\ - for (i = 0; i < paths.Count; i++) + var result = 0; + for (int i = 0; i < paths.Count; i++) { - j = 0; - p = paths[i]; - len = p.Count; + int j = 0; + List p = paths[i]; + int len = p.Count; + if (len < 3) continue; - prevPt = p[len - 1]; + ClipperIntPoint prevPt = p[len - 1]; + while ((j < len) && (p[j].Y == prevPt.Y)) j++; if (j == len) continue; - isAbove = (prevPt.Y < pt.Y); + + bool isAbove = prevPt.Y < pt.Y; + while (j < len) { if (isAbove) @@ -863,16 +862,15 @@ { prevPt = p[j - 1]; } - crossProd = CrossProduct(prevPt, p[j], pt); + + double crossProd = CrossProduct(prevPt, p[j], pt); if (crossProd == 0) { return int.MaxValue; - //result:= MaxInt; - //Exit; } else if (crossProd < 0) { - Result--; + result--; } } else @@ -886,23 +884,23 @@ { prevPt = p[j - 1]; } - crossProd = CrossProduct(prevPt, p[j], pt); + + double crossProd = CrossProduct(prevPt, p[j], pt); if (crossProd == 0) { return int.MaxValue; - //result:= MaxInt; - //Exit; } else if (crossProd > 0) { - Result++; + result++; } } + j++; isAbove = !isAbove; } } - return Result; + return result; } private static bool PointInPaths(ClipperIntPoint pt, List> paths, ClipperPolyFillType fillRule, bool includeBorder) @@ -915,11 +913,11 @@ switch (fillRule) { + default: case ClipperPolyFillType.EvenOdd: - return wc % 2 != 0; // Odd() + return wc % 2 != 0; case ClipperPolyFillType.NonZero: - default: return wc != 0; } } From 77d448c517b8dcefd649209d280488e00cf3a939 Mon Sep 17 00:00:00 2001 From: BobLd Date: Sun, 30 Aug 2020 13:57:02 +0100 Subject: [PATCH 4/6] better functions documentation --- .../Geometry/GeometryExtensions.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index 8aad608e..df2ceaf7 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -323,7 +323,7 @@ #region PdfRectangle /// - /// Whether the rectangle contains the point. + /// Whether the point is located inside the rectangle. /// /// The rectangle that should contain the point. /// The point that should be contained within the rectangle. @@ -372,7 +372,7 @@ } /// - /// Whether the rectangle contains the rectangle. + /// Whether the other rectangle is located inside the rectangle. /// /// The rectangle that should contain the other rectangle. /// The other rectangle that should be contained within the rectangle. @@ -480,7 +480,7 @@ #region PdfLine /// - /// Whether the line segment contains the point. + /// Whether the point is located on the line segment. /// public static bool Contains(this PdfLine line, PdfPoint point) { @@ -538,7 +538,7 @@ #region Path Line /// - /// Whether the line segment contains the point. + /// Whether the point is located on the line segment. /// public static bool Contains(this Line line, PdfPoint point) { @@ -924,7 +924,7 @@ #endregion /// - /// Whether the subpath contains the point. + /// Whether the point is located inside the subpath. /// Ignores winding rule. /// /// The subpath that should contain the point. @@ -939,7 +939,7 @@ } /// - /// Whether the subpath contains the rectangle. + /// Whether the rectangle is located inside the subpath. /// Ignores winding rule. /// /// The subpath that should contain the rectangle. @@ -956,7 +956,7 @@ } /// - /// Whether the subpath contains the other subpath. + /// Whether the other subpath is located inside the subpath. /// Ignores winding rule. /// /// The subpath that should contain the rectangle. @@ -989,7 +989,7 @@ } /// - /// Whether the path contains the point. + /// Whether the point is located inside the path. /// /// The path that should contain the point. /// The point that should be contained within the path. @@ -1004,7 +1004,7 @@ } /// - /// Whether the path contains the rectangle. + /// Whether the rectangle is located inside the path. /// /// The path that should contain the rectangle. /// The rectangle that should be contained within the path. @@ -1021,7 +1021,7 @@ } /// - /// Whether the path contains the subpath. + /// Whether the subpath is located inside the path. /// /// The path that should contain the subpath. /// The subpath that should be contained within the path. @@ -1038,7 +1038,7 @@ } /// - /// Whether the path contains the other path. + /// Whether the other path is located inside the path. /// /// The path that should contain the path. /// The other path that should be contained within the path. From e5ab4eb5718e0e46fcd00628dfbd553e9fbca173 Mon Sep 17 00:00:00 2001 From: BobLd Date: Mon, 31 Aug 2020 11:36:17 +0100 Subject: [PATCH 5/6] minor corrections --- .../Geometry/GeometryExtensions.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index df2ceaf7..c1cc481d 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -59,14 +59,14 @@ } /// - /// Algorithm to find a minimal bounding rectangle (MBR) such that the MBR corresponds to a rectangle + /// Algorithm to find a minimal bounding rectangle (MBR) such that the MBR corresponds to a rectangle /// with smallest possible area completely enclosing the polygon. /// From 'A Fast Algorithm for Generating a Minimal Bounding Rectangle' by Lennert D. Den Boer. /// /// /// Polygon P is assumed to be both simple and convex, and to contain no duplicate (coincident) vertices. - /// The vertices of P are assumed to be in strict cyclic sequential order, either clockwise or - /// counter-clockwise relative to the origin P0. + /// The vertices of P are assumed to be in strict cyclic sequential order, either clockwise or + /// counter-clockwise relative to the origin P0. /// private static PdfRectangle ParametricPerpendicularProjection(IReadOnlyList polygon) { @@ -182,7 +182,7 @@ return new PdfRectangle(new PdfPoint(MBR[4], MBR[5]), new PdfPoint(MBR[6], MBR[7]), - new PdfPoint(MBR[2], MBR[3]), + new PdfPoint(MBR[2], MBR[3]), new PdfPoint(MBR[0], MBR[1])); } @@ -193,7 +193,7 @@ /// The points. public static PdfRectangle MinimumAreaRectangle(IEnumerable points) { - if (points == null || points.Count() == 0) + if (points?.Any() != true) { throw new ArgumentException("MinimumAreaRectangle(): points cannot be null and must contain at least one point.", nameof(points)); } @@ -210,7 +210,7 @@ { if (points == null || points.Count < 2) { - throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points."); + throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points.", nameof(points)); } // Fitting a line through the points @@ -252,8 +252,7 @@ cos, sin, 0, -sin, cos, 0, 0, 0, 1); - var obb = rotateBack.Transform(aabb); - return obb; + return rotateBack.Transform(aabb); } /// @@ -261,9 +260,9 @@ /// public static IEnumerable GrahamScan(IEnumerable points) { - if (points == null || points.Count() == 0) + if (points?.Any() != true) { - throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point."); + throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point.", nameof(points)); } if (points.Count() < 3) return points; @@ -434,12 +433,12 @@ if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight, other.BottomRight, other.TopRight)) return true; if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight, other.TopRight, other.TopLeft)) return true; if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight,other.TopLeft, other.BottomLeft)) return true; - + if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.BottomLeft, other.BottomRight)) return true; if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight,other.BottomRight, other.TopRight)) return true; if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.TopRight, other.TopLeft)) return true; if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.TopLeft, other.BottomLeft)) return true; - + if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.BottomLeft, other.BottomRight)) return true; if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.BottomRight, other.TopRight)) return true; if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.TopRight, other.TopLeft)) return true; @@ -739,7 +738,7 @@ { return Intersect(bezierCurve, line.From, line.To); } - + private static PdfPoint[] Intersect(BezierCurve bezierCurve, PdfPoint p1, PdfPoint p2) { var ts = IntersectT(bezierCurve, p1, p2); @@ -814,7 +813,7 @@ var solution = SolveCubicEquation(a, b, c, d); - return solution.Where(s => !double.IsNaN(s)).Where(s => s >= -epsilon && (s - 1) <= epsilon).OrderBy(s => s).ToArray(); + return solution.Where(s => !double.IsNaN(s) && s >= -epsilon && (s - 1) <= epsilon).OrderBy(s => s).ToArray(); } #endregion @@ -1056,7 +1055,6 @@ } return true; } - #endregion private const double OneThird = 0.333333333333333333333; @@ -1182,8 +1180,7 @@ { string BboxToRect(PdfRectangle box, string stroke) { - var overallBbox = $""; - return overallBbox; + return $""; } var glyph = p.ToSvg(height); @@ -1202,9 +1199,7 @@ var path = $""; var bboxRect = bbox.HasValue ? BboxToRect(bbox.Value, "yellow") : string.Empty; var others = string.Join(" ", bboxes.Select(x => BboxToRect(x, "gray"))); - var result = $"{path} {bboxRect} {others}"; - - return result; + return $"{path} {bboxRect} {others}"; } } } From 7b085a849867e4232dcfdcc69b8cc009dd950b9b Mon Sep 17 00:00:00 2001 From: BobLd Date: Tue, 1 Sep 2020 11:18:22 +0100 Subject: [PATCH 6/6] update documentation and add note about concave poly issue --- .../Geometry/GeometryExtensions.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs index c1cc481d..58f48102 100644 --- a/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs +++ b/src/UglyToad.PdfPig/Geometry/GeometryExtensions.cs @@ -928,7 +928,7 @@ /// /// The subpath that should contain the point. /// The point that should be contained within the subpath. - /// If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the point belongs to the subpath's border. public static bool Contains(this PdfSubpath subpath, PdfPoint point, bool includeBorder = false) { return PointInPaths(point.ToClipperIntPoint(), @@ -943,14 +943,16 @@ /// /// The subpath that should contain the rectangle. /// The rectangle that should be contained within the subpath. - /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the rectangle is on the subpath's border. public static bool Contains(this PdfSubpath subpath, PdfRectangle rectangle, bool includeBorder = false) { + // NB, For later dev: Might not work for concave outer subpath, as it can contain all the points of the rectangle, but have overlapping edges. var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; - if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; - if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; - if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; - if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + foreach (var point in rectangle.ToClipperPolygon()) + { + if (!PointInPaths(point, clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false; + } + return true; } @@ -960,9 +962,10 @@ /// /// The subpath that should contain the rectangle. /// The other subpath that should be contained within the subpath. - /// [Not used for the moment] If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the other subpath is on the subpath's border. public static bool Contains(this PdfSubpath subpath, PdfSubpath other, bool includeBorder = false) { + // NB, For later dev: Might not work for concave outer subpath, as it can contain all the points of the inner subpath, but have overlapping edges. var clipperPaths = new List>() { subpath.ToClipperPolygon().ToList() }; foreach (var pt in other.ToClipperPolygon()) { @@ -992,7 +995,7 @@ /// /// The path that should contain the point. /// The point that should be contained within the path. - /// If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the point belongs to the path's border. public static bool Contains(this PdfPath path, PdfPoint point, bool includeBorder = false) { var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); @@ -1007,15 +1010,17 @@ /// /// The path that should contain the rectangle. /// The rectangle that should be contained within the path. - /// If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the rectangle is on the path's border. public static bool Contains(this PdfPath path, PdfRectangle rectangle, bool includeBorder = false) { + // NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner rectangle, but have overlapping edges. var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; - if (!PointInPaths(rectangle.BottomLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; - if (!PointInPaths(rectangle.TopLeft.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; - if (!PointInPaths(rectangle.TopRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; - if (!PointInPaths(rectangle.BottomRight.ToClipperIntPoint(), clipperPaths, fillType, includeBorder)) return false; + foreach (var point in rectangle.ToClipperPolygon()) + { + if (!PointInPaths(point, clipperPaths, fillType, includeBorder)) return false; + } + return true; } @@ -1024,9 +1029,10 @@ /// /// The path that should contain the subpath. /// The subpath that should be contained within the path. - /// If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the subpath is on the path's border. public static bool Contains(this PdfPath path, PdfSubpath subpath, bool includeBorder = false) { + // NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner subpath, but have overlapping edges. var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; foreach (var p in subpath.ToClipperPolygon()) @@ -1041,9 +1047,10 @@ /// /// The path that should contain the path. /// The other path that should be contained within the path. - /// If set to false, will return false if the point belongs to the border. + /// If set to false, will return false if the other subpath is on the path's border. public static bool Contains(this PdfPath path, PdfPath other, bool includeBorder = false) { + // NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner path, but have overlapping edges. var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList(); var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd; foreach (var subpath in other)