PdfPig/src/UglyToad.PdfPig.Core/PdfSubpath.cs
BobLd a7fe39fc32 Add SvgTextExporter
Modify WriteSvg to account for height
2020-04-05 17:58:57 +01:00

723 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace UglyToad.PdfPig.Core
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/// <summary>
///
/// </summary>
public class PdfSubpath
{
private readonly List<IPathCommand> commands = new List<IPathCommand>();
/// <summary>
/// The sequence of sub-paths which form this <see cref="PdfSubpath"/>.
/// </summary>
public IReadOnlyList<IPathCommand> Commands => commands;
/// <summary>
/// True if the <see cref="PdfSubpath"/> was originaly draw as a rectangle.
/// </summary>
public bool IsDrawnAsRectangle { get; internal set; }
private PdfPoint? currentPosition;
private double shoeLaceSum;
/// <summary>
/// Return true if points are organised in a clockwise order. Works only with closed paths.
/// </summary>
/// <returns></returns>
public bool IsClockwise
{
get
{
if (!IsClosed()) return false;
return shoeLaceSum > 0;
}
}
/// <summary>
/// Return true if points are organised in a counterclockwise order. Works only with closed paths.
/// </summary>
/// <returns></returns>
public bool IsCounterClockwise
{
get
{
if (!IsClosed()) return false;
return shoeLaceSum < 0;
}
}
/// <summary>
/// Get the <see cref="PdfSubpath"/>'s centroid point.
/// </summary>
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();
}
/// <summary>
/// Simplify this <see cref="PdfSubpath"/> by converting everything to <see cref="PdfLine"/>s.
/// </summary>
/// <param name="n">Number of lines required (minimum is 1).</param>
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;
}
/// <summary>
/// Add a <see cref="Move"/> command to the path.
/// </summary>
public void MoveTo(double x, double y)
{
currentPosition = new PdfPoint(x, y);
commands.Add(new Move(currentPosition.Value));
}
/// <summary>
/// Add a <see cref="Line"/> command to the path.
/// </summary>
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.");
}
}
/// <summary>
/// Adds 4 <see cref="Line"/>s forming a rectangle to the path.
/// </summary>
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) { }
/// <summary>
/// Add a <see cref="BezierCurve"/> to the path.
/// </summary>
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.");
}
}
/// <summary>
/// Close the path.
/// </summary>
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());
}
/// <summary>
/// Determines if the path is currently closed.
/// </summary>
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;
}
/// <summary>
/// Gets a <see cref="PdfRectangle"/> which entirely contains the geometry of the defined path.
/// </summary>
/// <returns>For paths which don't define any geometry this returns <see langword="null"/>.</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);
}
/// <summary>
/// A command in a <see cref="PdfSubpath"/>.
/// </summary>
public interface IPathCommand
{
/// <summary>
/// Returns the smallest rectangle which contains the path region given by this command.
/// </summary>
/// <returns></returns>
PdfRectangle? GetBoundingRectangle();
/// <summary>
/// Converts from the path command to an SVG string representing the path operation.
/// </summary>
void WriteSvg(StringBuilder builder, double height);
}
/// <summary>
/// Close the current <see cref="PdfSubpath"/>.
/// </summary>
public class Close : IPathCommand
{
/// <inheritdoc />
public PdfRectangle? GetBoundingRectangle()
{
return null;
}
/// <inheritdoc />
public void WriteSvg(StringBuilder builder, double height)
{
builder.Append("Z ");
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return (obj is Close);
}
/// <inheritdoc />
public override int GetHashCode()
{
// ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode
return base.GetHashCode();
}
}
/// <summary>
/// Move drawing of the current <see cref="PdfSubpath"/> to the specified location.
/// </summary>
public class Move : IPathCommand
{
/// <summary>
/// The location to move to.
/// </summary>
public PdfPoint Location { get; }
/// <summary>
/// Create a new <see cref="Move"/> path command.
/// </summary>
/// <param name="location"></param>
public Move(PdfPoint location)
{
Location = location;
}
/// <summary>
/// Returns <see langword="null"/> since this generates no visible path.
/// </summary>
public PdfRectangle? GetBoundingRectangle()
{
return null;
}
/// <inheritdoc />
public void WriteSvg(StringBuilder builder, double height)
{
builder.Append($"M {Location.X} {height - Location.Y} ");
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is Move move)
{
return Location.Equals(move.Location);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (Location).GetHashCode();
}
}
/// <summary>
/// Draw a straight line between two points.
/// </summary>
public class Line : IPathCommand
{
/// <summary>
/// The start of the line.
/// </summary>
public PdfPoint From { get; }
/// <summary>
/// The end of the line.
/// </summary>
public PdfPoint To { get; }
/// <summary>
/// Length of the line.
/// </summary>
public double Length
{
get
{
var dx = From.X - To.X;
var dy = From.Y - To.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
/// <summary>
/// Create a new <see cref="Line"/>.
/// </summary>
public Line(PdfPoint from, PdfPoint to)
{
From = from;
To = to;
}
/// <inheritdoc />
public PdfRectangle? GetBoundingRectangle()
{
return new PdfRectangle(From, To);
}
/// <inheritdoc />
public void WriteSvg(StringBuilder builder, double height)
{
builder.Append($"L {To.X} {height - To.Y} ");
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is Line line)
{
return From.Equals(line.From) && To.Equals(line.To);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (From, To).GetHashCode();
}
}
/// <summary>
/// Draw a Bezier curve given by the start, control and end points.
/// </summary>
public class BezierCurve : IPathCommand
{
/// <summary>
/// The start point of the Bezier curve.
/// </summary>
public PdfPoint StartPoint { get; }
/// <summary>
/// The first control point of the curve.
/// </summary>
public PdfPoint FirstControlPoint { get; }
/// <summary>
/// The second control point of the curve.
/// </summary>
public PdfPoint SecondControlPoint { get; }
/// <summary>
/// The end point of the curve.
/// </summary>
public PdfPoint EndPoint { get; }
/// <summary>
/// Create a Bezier curve at the provided points.
/// </summary>
public BezierCurve(PdfPoint startPoint, PdfPoint firstControlPoint, PdfPoint secondControlPoint, PdfPoint endPoint)
{
StartPoint = startPoint;
FirstControlPoint = firstControlPoint;
SecondControlPoint = secondControlPoint;
EndPoint = endPoint;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 = (1t)^3*P_1 + 3(1t)^2*t*P_2 + 3(1t)*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;
}
/// <summary>
/// Calculate the value of the Bezier curve at t.
/// </summary>
public static double ValueWithT(double p1, double p2, double p3, double p4, double t)
{
// P = (1t)^3*P_1 + 3(1t)^2*t*P_2 + 3(1t)*t^2*P_3 + t^3*P_4
var oneMinusT = 1 - t;
var p = ((oneMinusT * oneMinusT * oneMinusT) * p1)
+ (3 * (oneMinusT * oneMinusT) * t * p2)
+ (3 * oneMinusT * (t * t) * p3)
+ ((t * t * t) * p4);
return p;
}
/// <summary>
/// Converts the bezier curve into approximated lines.
/// </summary>
/// <param name="n">Number of lines required (minimum is 1).</param>
/// <returns></returns>
public IReadOnlyList<Line> ToLines(int n)
{
if (n < 1)
{
throw new ArgumentException("BezierCurve.ToLines(): n must be greater than 0.");
}
List<Line> lines = new List<Line>();
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override int GetHashCode()
{
return (StartPoint, FirstControlPoint, SecondControlPoint, EndPoint).GetHashCode();
}
}
/// <summary>
/// Compares two <see cref="PdfSubpath"/>s for equality. Paths will only be considered equal if the commands which construct the paths are in the same order.
/// </summary>
public override bool Equals(object obj)
{
if (obj is 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;
}
/// <summary>
/// Get the hash code. Paths will only have the same hash code if the commands which construct the paths are in the same order.
/// </summary>
public override int GetHashCode()
{
var hash = Commands.Count + 1;
for (int i = 0; i < Commands.Count; i++)
{
hash = hash * (i + 1) * 17 + Commands[i].GetHashCode();
}
return hash;
}
}
}