mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
support other types of callothersubr. calculate bounding rectangle from glyph path
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = (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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user