diff --git a/src/UglyToad.PdfPig.Tests/Fonts/Type1/CharStrings/CharacterPathTests.cs b/src/UglyToad.PdfPig.Tests/Fonts/Type1/CharStrings/CharacterPathTests.cs
new file mode 100644
index 00000000..9e5cbaa6
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Fonts/Type1/CharStrings/CharacterPathTests.cs
@@ -0,0 +1,61 @@
+namespace UglyToad.PdfPig.Tests.Fonts.Type1.CharStrings
+{
+ using System.Text;
+ using PdfPig.Fonts.Type1.CharStrings;
+ using PdfPig.Geometry;
+ using Xunit;
+
+ public class CharacterPathTests
+ {
+ [Fact]
+ public void BezierCurveGeneratesCorrectBoundingBox()
+ {
+ var curve = new CharacterPath.BezierCurve(new PdfPoint(60, 105),
+ new PdfPoint(75, 30),
+ new PdfPoint(215, 115),
+ new PdfPoint(140, 160));
+
+ var result = curve.GetBoundingRectangle();
+ Assert.NotNull(result);
+ Assert.Equal(160, result.Value.Top);
+ // Extends beyond start but not as far as 1st control point.
+ Assert.True(result.Value.Bottom < 105 && result.Value.Bottom > 30);
+ // Extends beyond end but not as far as 2nd control point.
+ Assert.True(result.Value.Right > 140 && result.Value.Right < 215);
+ Assert.Equal(60, result.Value.Left);
+ }
+
+ [Fact]
+ public void LoopBezierCurveGeneratesCorrectBoundingBox()
+ {
+ var curve = new CharacterPath.BezierCurve(new PdfPoint(166, 142),
+ new PdfPoint(75, 30),
+ new PdfPoint(215, 115),
+ new PdfPoint(140, 160));
+
+ var result = curve.GetBoundingRectangle();
+
+ Assert.NotNull(result);
+ Assert.Equal(160, result.Value.Top);
+ // Extends beyond start but not as far as 1st control point.
+ Assert.True(result.Value.Bottom < 142 && result.Value.Bottom > 30);
+ Assert.Equal(166, result.Value.Right);
+ // Extends beyond end.
+ Assert.True(result.Value.Left < 140);
+ }
+
+ [Fact]
+ public void BezierCurveAddsCorrectSvgCommand()
+ {
+ var curve = new CharacterPath.BezierCurve(new PdfPoint(60, 105),
+ new PdfPoint(75, 30),
+ new PdfPoint(215, 115),
+ new PdfPoint(140, 160));
+
+ var builder = new StringBuilder();
+ curve.WriteSvg(builder);
+
+ Assert.Equal("C 75 30, 215 115, 140 160 ", builder.ToString());
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Fonts/Simple/Type1FontSimple.cs b/src/UglyToad.PdfPig/Fonts/Simple/Type1FontSimple.cs
index ec4c8340..7849248c 100644
--- a/src/UglyToad.PdfPig/Fonts/Simple/Type1FontSimple.cs
+++ b/src/UglyToad.PdfPig/Fonts/Simple/Type1FontSimple.cs
@@ -107,9 +107,9 @@
return new PdfRectangle(0, 0, 250, 0);
}
- this.fontProgram.GetCharacterBoundingBox(characterCode);
+ var rect = fontProgram.GetCharacterBoundingBox(characterCode);
- return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0);
+ return rect;
}
public TransformationMatrix GetFontMatrix()
diff --git a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/CharacterPath.cs b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/CharacterPath.cs
index 0e293a85..e2f14519 100644
--- a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/CharacterPath.cs
+++ b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/CharacterPath.cs
@@ -1,6 +1,9 @@
-namespace UglyToad.PdfPig.Fonts.Type1.CharStrings
+// ReSharper disable ArrangeRedundantParentheses
+namespace UglyToad.PdfPig.Fonts.Type1.CharStrings
{
+ using System;
using System.Collections.Generic;
+ using System.Linq;
using System.Text;
using Geometry;
@@ -55,7 +58,42 @@
public PdfRectangle GetBoundingRectangle()
{
- return new PdfRectangle();
+ var minX = decimal.MaxValue;
+ var maxX = decimal.MinValue;
+
+ var minY = decimal.MaxValue;
+ var maxY = decimal.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;
+ }
+ }
+
+ return new PdfRectangle(minX, minY, maxX, maxY);
}
public string ToSvg()
@@ -79,13 +117,49 @@
return builder.ToString();
}
- private interface IPathCommand
+ public string ToFullSvg()
{
+ string BboxToRect(PdfRectangle box, string stroke)
+ {
+ var overallBbox = $"";
+ return overallBbox;
+ }
+
+ var glyph = ToSvg();
+ var bbox = GetBoundingRectangle();
+ var bboxes = new List();
+
+ foreach (var command in commands)
+ {
+ var segBbox = command.GetBoundingRectangle();
+ if (segBbox.HasValue)
+ {
+ bboxes.Add(segBbox.Value);
+ }
+ }
+
+ var path = $"";
+ var bboxRect = BboxToRect(bbox, "yellow");
+ var others = string.Join(" ", bboxes.Select(x => BboxToRect(x, "gray")));
+ var result = $"";
+
+ return result;
+ }
+
+ public interface IPathCommand
+ {
+ PdfRectangle? GetBoundingRectangle();
+
void WriteSvg(StringBuilder builder);
}
private class Close : IPathCommand
{
+ public PdfRectangle? GetBoundingRectangle()
+ {
+ return null;
+ }
+
public void WriteSvg(StringBuilder builder)
{
builder.Append("Z ");
@@ -101,6 +175,11 @@
Location = location;
}
+ public PdfRectangle? GetBoundingRectangle()
+ {
+ return null;
+ }
+
public void WriteSvg(StringBuilder builder)
{
builder.Append("M ").Append(Location.X).Append(' ').Append(Location.Y).Append(' ');
@@ -119,13 +198,18 @@
To = to;
}
+ public PdfRectangle? GetBoundingRectangle()
+ {
+ return new PdfRectangle(From, To);
+ }
+
public void WriteSvg(StringBuilder builder)
{
builder.AppendFormat("L {0} {1} ", To.X, To.Y);
}
}
- private class BezierCurve : IPathCommand
+ public class BezierCurve : IPathCommand
{
public PdfPoint StartPoint { get; }
@@ -143,6 +227,112 @@
EndPoint = endPoint;
}
+ public PdfRectangle? GetBoundingRectangle()
+ {
+ var minX = Math.Min(StartPoint.X, EndPoint.X);
+ var maxX = Math.Max(StartPoint.X, EndPoint.X);
+
+ var minY = Math.Min(StartPoint.Y, EndPoint.Y);
+ var maxY = Math.Max(StartPoint.Y, EndPoint.Y);
+
+ if (TrySolveQuadratic(x => (double)x.X, minX, maxX, out var xSolutions))
+ {
+ minX = xSolutions.min;
+ maxX = xSolutions.max;
+ }
+
+ if (TrySolveQuadratic(x => (double)x.Y, minY, maxY, out var ySolutions))
+ {
+ minY = ySolutions.min;
+ maxY = ySolutions.max;
+ }
+
+ return new PdfRectangle(minX, minY, maxX, maxY);
+ }
+
+
+ private bool TrySolveQuadratic(Func valueAccessor, decimal currentMin, decimal currentMax, out (decimal min, decimal max) solutions)
+ {
+ solutions = default((decimal, decimal));
+
+ // 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 = valueAccessor(StartPoint);
+ var p2 = valueAccessor(FirstControlPoint);
+ var p3 = valueAccessor(SecondControlPoint);
+ var p4 = valueAccessor(EndPoint);
+
+ 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 t1 = (-b + Math.Sqrt(sqrtable)) / (2 * a);
+ var t2 = (-b - Math.Sqrt(sqrtable)) / (2 * a);
+
+ if (t1 >= 0 && t1 <= 1)
+ {
+ var sol1 = (decimal)ValueWithT(p1, p2, p3, p4, t1);
+ if (sol1 < currentMin)
+ {
+ currentMin = sol1;
+ }
+
+ if (sol1 > currentMax)
+ {
+ currentMax = sol1;
+ }
+ }
+
+ if (t2 >= 0 && t2 <= 1)
+ {
+ var sol2 = (decimal)ValueWithT(p1, p2, p3, p4, t2);
+ if (sol2 < currentMin)
+ {
+ currentMin = sol2;
+ }
+
+ if (sol2 > currentMax)
+ {
+ currentMax = sol2;
+ }
+ }
+
+ solutions = (currentMin, currentMax);
+
+ return true;
+ }
+
+ private 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 p = (Math.Pow(1 - t, 3) * p1) + (3 * Math.Pow(1 - t, 2) * t * p2) + (3 * (1 - t) * Math.Pow(t, 2) * p3) + (Math.Pow(t, 3) * p4);
+
+ return p;
+ }
+
public void WriteSvg(StringBuilder builder)
{
builder.AppendFormat("C {0} {1}, {2} {3}, {4} {5} ", FirstControlPoint.X, FirstControlPoint.Y, SecondControlPoint.X, SecondControlPoint.Y,
diff --git a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Commands/Arithmetic/CallOtherSubrCommand.cs b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Commands/Arithmetic/CallOtherSubrCommand.cs
index bb816108..68568113 100644
--- a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Commands/Arithmetic/CallOtherSubrCommand.cs
+++ b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Commands/Arithmetic/CallOtherSubrCommand.cs
@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
+ using System.Diagnostics;
///
/// Call other subroutine command. Arguments are pushed onto the PostScript interpreter operand stack then
@@ -10,6 +11,11 @@
///
internal static class CallOtherSubrCommand
{
+ private const int FlexEnd = 0;
+ private const int FlexBegin = 1;
+ private const int FlexMiddle = 2;
+ private const int HintReplacement = 3;
+
public const string Name = "callothersubr";
public static readonly byte First = 12;
@@ -25,16 +31,17 @@
var index = (int) context.Stack.PopTop();
// What it should do
- //var numberOfArguments = (int)context.Stack.PopTop();
- //var otherSubroutineArguments = new List(numberOfArguments);
- //for (int j = 0; j < numberOfArguments; j++)
- //{
- // otherSubroutineArguments.Add(context.Stack.PopTop());
- //}
+ var numberOfArguments = (int)context.Stack.PopTop();
+ var otherSubroutineArguments = new List(numberOfArguments);
+ for (int j = 0; j < numberOfArguments; j++)
+ {
+ otherSubroutineArguments.Add(context.Stack.PopTop());
+ }
switch (index)
{
- case 0:
+ // Other subrs 0-2 implement flex
+ case FlexEnd:
{
context.IsFlexing = false;
if (context.FlexPoints.Count < 7)
@@ -45,9 +52,38 @@
context.ClearFlexPoints();
break;
}
- case 1:
+ case FlexBegin:
+ Debug.Assert(otherSubroutineArguments.Count == 0, "Flex begin should have no arguments.");
+
+ context.PostscriptStack.Clear();
+ context.PostscriptStack.Push(context.CurrentPosition.X);
+ context.PostscriptStack.Push(context.CurrentPosition.Y);
context.IsFlexing = true;
break;
+ case FlexMiddle:
+ Debug.Assert(otherSubroutineArguments.Count == 0, "Flex middle should have no arguments.");
+
+ context.PostscriptStack.Push(context.CurrentPosition.X);
+ context.PostscriptStack.Push(context.CurrentPosition.Y);
+ break;
+ // Other subrs 3 implements hint replacement
+ case HintReplacement:
+ if (otherSubroutineArguments.Count != 1)
+ {
+ throw new InvalidOperationException("The hint replacement subroutine only takes a single argument.");
+ }
+
+ context.PostscriptStack.Clear();
+ context.PostscriptStack.Push(otherSubroutineArguments[0]);
+ break;
+ default:
+ // Other subrs beyond the first 4 can safely be ignored.
+ context.PostscriptStack.Clear();
+ for (var i = 0; i < otherSubroutineArguments.Count; i++)
+ {
+ context.PostscriptStack.Push(otherSubroutineArguments[i]);
+ }
+ break;
}
}
}
diff --git a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Type1CharStrings.cs b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Type1CharStrings.cs
index 6bed7be7..c6110a1b 100644
--- a/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Type1CharStrings.cs
+++ b/src/UglyToad.PdfPig/Fonts/Type1/CharStrings/Type1CharStrings.cs
@@ -9,51 +9,51 @@
internal class Type1CharStrings
{
private readonly object locker = new object();
- private readonly Dictionary charStrings = new Dictionary();
+ private readonly Dictionary glyphs = new Dictionary();
public IReadOnlyDictionary CharStrings { get; }
public IReadOnlyDictionary Subroutines { get; }
-
+
public Type1CharStrings(IReadOnlyDictionary charStrings, IReadOnlyDictionary subroutines)
{
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
Subroutines = subroutines ?? throw new ArgumentNullException(nameof(subroutines));
}
- public void Generate(string name)
+ public CharacterPath Generate(string name)
{
+ CharacterPath glyph;
lock (locker)
{
- if (charStrings.TryGetValue(name, out var result))
+ if (glyphs.TryGetValue(name, out var result))
{
- return;
+ return result;
}
+
+ if (!CharStrings.TryGetValue(name, out var sequence))
+ {
+ throw new InvalidOperationException($"No charstring sequence with the name /{name} in this font.");
+ }
+
+ glyph = Run(sequence);
+
+ glyphs[name] = glyph;
}
- if (!CharStrings.TryGetValue(name, out var sequence))
- {
- throw new InvalidOperationException($"No charstring sequence with the name /{name} in this font.");
- }
-
- Run(sequence);
-
- lock (locker)
- {
- charStrings[name] = sequence;
- }
+ return glyph;
}
- private void Run(CommandSequence sequence)
+ private static CharacterPath Run(CommandSequence sequence)
{
var context = new Type1BuildCharContext();
foreach (var command in sequence.Commands)
{
- command.Match(x => context.Stack.Push(x),
+ command.Match(x => context.Stack.Push(x),
x => x.Run(context));
}
- var str = context.Path.ToSvg();
+ return context.Path;
}
public class CommandSequence
diff --git a/src/UglyToad.PdfPig/Fonts/Type1/Type1FontProgram.cs b/src/UglyToad.PdfPig/Fonts/Type1/Type1FontProgram.cs
index cbdb66a3..a5d6dab8 100644
--- a/src/UglyToad.PdfPig/Fonts/Type1/Type1FontProgram.cs
+++ b/src/UglyToad.PdfPig/Fonts/Type1/Type1FontProgram.cs
@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
+ using System.Diagnostics;
using CharStrings;
using Geometry;
using Tokenization.Tokens;
@@ -61,8 +62,24 @@
public PdfRectangle GetCharacterBoundingBox(int characterCode)
{
var b = Encoding[characterCode];
- CharStrings.Generate(b);
- return new PdfRectangle();
+ var glyph = CharStrings.Generate(b);
+ var bbox = glyph.GetBoundingRectangle();
+
+ if (Debugger.IsAttached)
+ {
+ if (bbox.Bottom < BoundingBox.Bottom
+ || bbox.Top > BoundingBox.Top
+ || bbox.Left < BoundingBox.Left
+ || bbox.Right > BoundingBox.Right)
+ {
+ // Debugger.Break();
+ }
+
+ var full = glyph.ToFullSvg();
+ Console.WriteLine(full);
+ }
+
+ return bbox;
}
}
}
\ No newline at end of file