namespace UglyToad.PdfPig.Core { using System; using System.Collections.Generic; using System.Linq; using System.Text; /// /// /// public class PdfSubpath { private readonly List commands = new List(); /// /// The sequence of sub-paths which form this . /// public IReadOnlyList Commands => commands; /// /// True if the was originaly draw as a rectangle. /// public bool IsDrawnAsRectangle { get; internal set; } private PdfPoint? currentPosition; private double shoeLaceSum; /// /// Return true if points are organised in a clockwise order. Works only with closed paths. /// /// public bool IsClockwise { get { if (!IsClosed()) return false; return shoeLaceSum > 0; } } /// /// Return true if points are organised in a counterclockwise order. Works only with closed paths. /// /// public bool IsCounterClockwise { get { if (!IsClosed()) return false; return shoeLaceSum < 0; } } /// /// Get the 's centroid point. /// public PdfPoint GetCentroid() { var filtered = commands.Where(c => c is Line || c is BezierCurve).ToList(); if (filtered.Count == 0) return new PdfPoint(); var points = filtered.Select(GetStartPoint).ToList(); points.AddRange(filtered.Select(GetEndPoint)); return new PdfPoint(points.Average(p => p.X), points.Average(p => p.Y)); } internal static PdfPoint GetStartPoint(IPathCommand command) { if (command is Line line) { return line.From; } if (command is BezierCurve curve) { return curve.StartPoint; } if (command is Move move) { return move.Location; } throw new ArgumentException(); } internal static PdfPoint GetEndPoint(IPathCommand command) { if (command is Line line) { return line.To; } if (command is BezierCurve curve) { return curve.EndPoint; } if (command is Move move) { return move.Location; } throw new ArgumentException(); } /// /// Simplify this by converting everything to s. /// /// Number of lines required (minimum is 1). internal PdfSubpath Simplify(int n = 4) { PdfSubpath simplifiedPath = new PdfSubpath(); var startPoint = GetStartPoint(Commands.First()); simplifiedPath.MoveTo(startPoint.X, startPoint.Y); foreach (var command in Commands) { if (command is Line line) { simplifiedPath.LineTo(line.To.X, line.To.Y); } else if (command is BezierCurve curve) { foreach (var lineB in curve.ToLines(n)) { simplifiedPath.LineTo(lineB.To.X, lineB.To.Y); } } } // Check if Closed, if yes: make sure it is actually closed (last TO = first FROM) if (IsClosed()) { var first = GetStartPoint(simplifiedPath.Commands.First()); if (!first.Equals(GetEndPoint(simplifiedPath.Commands.Last()))) { simplifiedPath.LineTo(first.X, first.Y); } } return simplifiedPath; } /// /// Add a command to the path. /// public void MoveTo(double x, double y) { currentPosition = new PdfPoint(x, y); commands.Add(new Move(currentPosition.Value)); } /// /// Add a command to the path. /// public void LineTo(double x, double y) { if (currentPosition.HasValue) { shoeLaceSum += ((x - currentPosition.Value.X) * (y + currentPosition.Value.Y)); var to = new PdfPoint(x, y); commands.Add(new Line(currentPosition.Value, to)); currentPosition = to; } else { // PDF Reference 1.7 p226 throw new ArgumentNullException("LineTo(): currentPosition is null."); } } /// /// Adds 4 s forming a rectangle to the path. /// public void Rectangle(double x, double y, double width, double height) { // is equivalent to MoveTo(x, y); // x y m LineTo(x + width, y); // (x + width) y l LineTo(x + width, y + height); // (x + width) (y + height) l LineTo(x, y + height); // x (y + height) l ClosePath(); // h IsDrawnAsRectangle = true; } internal void QuadraticCurveTo(double x1, double y1, double x2, double y2) { } /// /// Add a to the path. /// public void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3) { if (currentPosition.HasValue) { shoeLaceSum += (x1 - currentPosition.Value.X) * (y1 + currentPosition.Value.Y); shoeLaceSum += (x2 - x1) * (y2 + y1); shoeLaceSum += (x3 - x2) * (y3 + y2); var to = new PdfPoint(x3, y3); commands.Add(new BezierCurve(currentPosition.Value, new PdfPoint(x1, y1), new PdfPoint(x2, y2), to)); currentPosition = to; } else { // PDF Reference 1.7 p226 throw new ArgumentNullException("BezierCurveTo(): currentPosition is null."); } } /// /// Close the path. /// public void ClosePath() { if (currentPosition.HasValue) { var startPoint = GetStartPoint(commands.First()); if (!startPoint.Equals(currentPosition.Value)) { shoeLaceSum += (startPoint.X - currentPosition.Value.X) * (startPoint.Y + currentPosition.Value.Y); } } commands.Add(new Close()); } /// /// Determines if the path is currently closed. /// public bool IsClosed() { // need to check if filled -> true if filled if (Commands.Any(c => c is Close)) return true; var filtered = Commands.Where(c => c is Line || c is BezierCurve).ToList(); if (filtered.Count < 2) return false; if (!GetStartPoint(filtered.First()).Equals(GetEndPoint(filtered.Last()))) return false; return true; } /// /// Gets a which entirely contains the geometry of the defined path. /// /// For paths which don't define any geometry this returns . public PdfRectangle? GetBoundingRectangle() { if (commands.Count == 0) { return null; } var minX = double.MaxValue; var maxX = double.MinValue; var minY = double.MaxValue; var maxY = double.MinValue; foreach (var command in commands) { var rect = command.GetBoundingRectangle(); if (rect == null) { continue; } if (rect.Value.Left < minX) { minX = rect.Value.Left; } if (rect.Value.Right > maxX) { maxX = rect.Value.Right; } if (rect.Value.Bottom < minY) { minY = rect.Value.Bottom; } if (rect.Value.Top > maxY) { maxY = rect.Value.Top; } } // ReSharper disable CompareOfFloatsByEqualityOperator if (minX == double.MaxValue || maxX == double.MinValue || minY == double.MaxValue || maxY == double.MinValue) { return null; } // ReSharper restore CompareOfFloatsByEqualityOperator return new PdfRectangle(minX, minY, maxX, maxY); } /// /// A command in a . /// public interface IPathCommand { /// /// Returns the smallest rectangle which contains the path region given by this command. /// /// PdfRectangle? GetBoundingRectangle(); /// /// Converts from the path command to an SVG string representing the path operation. /// void WriteSvg(StringBuilder builder, double height); } /// /// Close the current . /// public class Close : IPathCommand { /// public PdfRectangle? GetBoundingRectangle() { return null; } /// public void WriteSvg(StringBuilder builder, double height) { builder.Append("Z "); } /// public override bool Equals(object obj) { return (obj is Close); } /// public override int GetHashCode() { // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode return base.GetHashCode(); } } /// /// Move drawing of the current to the specified location. /// public class Move : IPathCommand { /// /// The location to move to. /// public PdfPoint Location { get; } /// /// Create a new path command. /// /// public Move(PdfPoint location) { Location = location; } /// /// Returns since this generates no visible path. /// public PdfRectangle? GetBoundingRectangle() { return null; } /// public void WriteSvg(StringBuilder builder, double height) { builder.Append($"M {Location.X} {height - Location.Y} "); } /// public override bool Equals(object obj) { if (obj is Move move) { return Location.Equals(move.Location); } return false; } /// public override int GetHashCode() { return (Location).GetHashCode(); } } /// /// Draw a straight line between two points. /// public class Line : IPathCommand { /// /// The start of the line. /// public PdfPoint From { get; } /// /// The end of the line. /// public PdfPoint To { get; } /// /// Length of the line. /// public double Length { get { var dx = From.X - To.X; var dy = From.Y - To.Y; return Math.Sqrt(dx * dx + dy * dy); } } /// /// Create a new . /// public Line(PdfPoint from, PdfPoint to) { From = from; To = to; } /// public PdfRectangle? GetBoundingRectangle() { return new PdfRectangle(From, To); } /// public void WriteSvg(StringBuilder builder, double height) { builder.Append($"L {To.X} {height - To.Y} "); } /// public override bool Equals(object obj) { if (obj is Line line) { return From.Equals(line.From) && To.Equals(line.To); } return false; } /// public override int GetHashCode() { return (From, To).GetHashCode(); } } /// /// Draw a Bezier curve given by the start, control and end points. /// public class BezierCurve : IPathCommand { /// /// The start point of the Bezier curve. /// public PdfPoint StartPoint { get; } /// /// The first control point of the curve. /// public PdfPoint FirstControlPoint { get; } /// /// The second control point of the curve. /// public PdfPoint SecondControlPoint { get; } /// /// The end point of the curve. /// public PdfPoint EndPoint { get; } /// /// Create a Bezier curve at the provided points. /// public BezierCurve(PdfPoint startPoint, PdfPoint firstControlPoint, PdfPoint secondControlPoint, PdfPoint endPoint) { StartPoint = startPoint; FirstControlPoint = firstControlPoint; SecondControlPoint = secondControlPoint; EndPoint = endPoint; } /// public PdfRectangle? GetBoundingRectangle() { // Optimised double minX; double maxX; if (StartPoint.X <= EndPoint.X) { minX = StartPoint.X; maxX = EndPoint.X; } else { minX = EndPoint.X; maxX = StartPoint.X; } double minY; double maxY; if (StartPoint.Y <= EndPoint.Y) { minY = StartPoint.Y; maxY = EndPoint.Y; } else { minY = EndPoint.Y; maxY = StartPoint.Y; } if (TrySolveQuadratic(true, minX, maxX, out var xSolutions)) { minX = xSolutions.min; maxX = xSolutions.max; } if (TrySolveQuadratic(false, minY, maxY, out var ySolutions)) { minY = ySolutions.min; maxY = ySolutions.max; } return new PdfRectangle(minX, minY, maxX, maxY); } /// public void WriteSvg(StringBuilder builder, double height) { builder.Append($"C {FirstControlPoint.X} { height - FirstControlPoint.Y}, { SecondControlPoint.X} {height - SecondControlPoint.Y}, {EndPoint.X} {height - EndPoint.Y} "); } private bool TrySolveQuadratic(bool isX, double currentMin, double currentMax, out (double min, double max) solutions) { solutions = default((double, double)); // This method has been optimised for performance by eliminating calls to Math. // Given k points the general form is: // P = (1-t)^(k - i - 1)*t^(i)*P_i // // For 4 points this gives: // P = (1−t)^3*P_1 + 3(1−t)^2*t*P_2 + 3(1−t)*t^2*P_3 + t^3*P_4 // The differential is: // P' = 3(1-t)^2(P_2 - P_1) + 6(1-t)^t(P_3 - P_2) + 3t^2(P_4 - P_3) // P' = 3da(1-t)^2 + 6db(1-t)t + 3dct^2 // P' = 3da - 3dat - 3dat + 3dat^2 + 6dbt - 6dbt^2 + 3dct^2 // P' = (3da - 6db + 3dc)t^2 + (6db - 3da - 3da)t + 3da var p1 = isX ? StartPoint.X : StartPoint.Y; var p2 = isX ? FirstControlPoint.X : FirstControlPoint.Y; var p3 = isX ? SecondControlPoint.X : SecondControlPoint.Y; var p4 = isX ? EndPoint.X : EndPoint.Y; var threeda = 3 * (p2 - p1); var sixdb = 6 * (p3 - p2); var threedc = 3 * (p4 - p3); var a = threeda - sixdb + threedc; var b = sixdb - threeda - threeda; var c = threeda; // P' = at^2 + bt + c // t = (-b (+/-) sqrt(b ^ 2 - 4ac))/2a var sqrtable = b * b - 4 * a * c; if (sqrtable < 0) { return false; } var sqrt = Math.Sqrt(sqrtable); var divisor = 2 * a; var t1 = (-b + sqrt) / divisor; var t2 = (-b - sqrt) / divisor; if (t1 >= 0 && t1 <= 1) { var sol1 = ValueWithT(p1, p2, p3, p4, t1); if (sol1 < currentMin) { currentMin = sol1; } if (sol1 > currentMax) { currentMax = sol1; } } if (t2 >= 0 && t2 <= 1) { var sol2 = ValueWithT(p1, p2, p3, p4, t2); if (sol2 < currentMin) { currentMin = sol2; } if (sol2 > currentMax) { currentMax = sol2; } } solutions = (currentMin, currentMax); return true; } /// /// Calculate the value of the Bezier curve at t. /// public 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; var p = ((oneMinusT * oneMinusT * oneMinusT) * p1) + (3 * (oneMinusT * oneMinusT) * t * p2) + (3 * oneMinusT * (t * t) * p3) + ((t * t * t) * p4); return p; } /// /// Converts the bezier curve into approximated lines. /// /// Number of lines required (minimum is 1). /// public IReadOnlyList ToLines(int n) { if (n < 1) { throw new ArgumentException("BezierCurve.ToLines(): n must be greater than 0."); } List lines = new List(); var previousPoint = StartPoint; for (int p = 1; p <= n; p++) { double t = p / (double)n; var currentPoint = new PdfPoint(ValueWithT(StartPoint.X, FirstControlPoint.X, SecondControlPoint.X, EndPoint.X, t), ValueWithT(StartPoint.Y, FirstControlPoint.Y, SecondControlPoint.Y, EndPoint.Y, t)); lines.Add(new Line(previousPoint, currentPoint)); previousPoint = currentPoint; } return lines; } /// public override bool Equals(object obj) { if (obj is BezierCurve curve) { return StartPoint.Equals(curve.StartPoint) && FirstControlPoint.Equals(curve.FirstControlPoint) && SecondControlPoint.Equals(curve.SecondControlPoint) && EndPoint.Equals(curve.EndPoint); } return false; } /// public override int GetHashCode() { return (StartPoint, FirstControlPoint, SecondControlPoint, EndPoint).GetHashCode(); } } /// /// 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) { if (obj is PdfSubpath path) { if (Commands.Count != path.Commands.Count) return false; for (int i = 0; i < Commands.Count; i++) { if (!Commands[i].Equals(path.Commands[i])) return false; } return true; } return false; } /// /// 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 = Commands.Count + 1; for (int i = 0; i < Commands.Count; i++) { hash = hash * (i + 1) * 17 + Commands[i].GetHashCode(); } return hash; } } }