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;
}
}
}