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 = $"{path} {bboxRect} {others}"; + + 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