support other types of callothersubr. calculate bounding rectangle from glyph path

This commit is contained in:
Eliot Jones
2018-11-14 22:22:51 +00:00
parent b477b3b560
commit 64a35e3217
6 changed files with 339 additions and 35 deletions

View File

@@ -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());
}
}
}

View File

@@ -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()

View File

@@ -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 = $"<rect x='{box.Left}' y='{box.Bottom}' width='{box.Width}' height='{box.Height}' stroke-width='2' fill='none' stroke='{stroke}'></rect>";
return overallBbox;
}
var glyph = ToSvg();
var bbox = GetBoundingRectangle();
var bboxes = new List<PdfRectangle>();
foreach (var command in commands)
{
var segBbox = command.GetBoundingRectangle();
if (segBbox.HasValue)
{
bboxes.Add(segBbox.Value);
}
}
var path = $"<path d='{glyph}' stroke='cyan' stroke-width='3'></path>";
var bboxRect = BboxToRect(bbox, "yellow");
var others = string.Join(" ", bboxes.Select(x => BboxToRect(x, "gray")));
var result = $"<svg transform='scale(1, -1)' width='2000' height='2000'>{path} {bboxRect} {others}</svg>";
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<PdfPoint, double> 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 = (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 = 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 = (1t)^3*P_1 + 3(1t)^2*t*P_2 + 3(1t)*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,

View File

@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
/// <summary>
/// Call other subroutine command. Arguments are pushed onto the PostScript interpreter operand stack then
@@ -10,6 +11,11 @@
/// </summary>
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<decimal>(numberOfArguments);
//for (int j = 0; j < numberOfArguments; j++)
//{
// otherSubroutineArguments.Add(context.Stack.PopTop());
//}
var numberOfArguments = (int)context.Stack.PopTop();
var otherSubroutineArguments = new List<decimal>(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;
}
}
}

View File

@@ -9,51 +9,51 @@
internal class Type1CharStrings
{
private readonly object locker = new object();
private readonly Dictionary<string, CommandSequence> charStrings = new Dictionary<string, CommandSequence>();
private readonly Dictionary<string, CharacterPath> glyphs = new Dictionary<string, CharacterPath>();
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
public IReadOnlyDictionary<int, CommandSequence> Subroutines { get; }
public Type1CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, IReadOnlyDictionary<int, CommandSequence> 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

View File

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