diff --git a/src/UglyToad.PdfPig.Core/PdfSubpath.cs b/src/UglyToad.PdfPig.Core/PdfSubpath.cs
index 4ed5b413..309c14b4 100644
--- a/src/UglyToad.PdfPig.Core/PdfSubpath.cs
+++ b/src/UglyToad.PdfPig.Core/PdfSubpath.cs
@@ -196,7 +196,7 @@
throw new ArgumentNullException("BezierCurveTo(): currentPosition is null.");
}
}
-
+
///
/// Close the path.
///
@@ -212,7 +212,7 @@
}
commands.Add(new Close());
}
-
+
///
/// Determines if the path is currently closed.
///
@@ -350,6 +350,30 @@
return new PdfRectangle(mv.Location, new PdfPoint(mv.Location.X + width, mv.Location.Y + height));
}
+ ///
+ /// Gets a which entirely contains the geometry of the defined path.
+ ///
+ /// For paths which don't define any geometry this returns .
+ public static PdfRectangle? GetBoundingRectangle(IReadOnlyList path)
+ {
+ if (path == null || path.Count == 0)
+ {
+ return null;
+ }
+
+ var bboxes = path.Select(x => x.GetBoundingRectangle()).Where(x => x.HasValue).Select(x => x.Value).ToList();
+ if (bboxes.Count == 0)
+ {
+ return null;
+ }
+
+ var minX = bboxes.Min(x => x.Left);
+ var minY = bboxes.Min(x => x.Bottom);
+ var maxX = bboxes.Max(x => x.Right);
+ var maxY = bboxes.Max(x => x.Top);
+ return new PdfRectangle(minX, minY, maxX, maxY);
+ }
+
///
/// A command in a .
///
diff --git a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
index b5b46336..75b51ed4 100644
--- a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
+++ b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
@@ -1,8 +1,11 @@
namespace UglyToad.PdfPig.Core
{
using System;
+ using System.Collections;
+ using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
+ using static UglyToad.PdfPig.Core.PdfSubpath;
///
/// Specifies the conversion from the transformed coordinate space to the original untransformed coordinate space.
@@ -256,6 +259,59 @@
);
}
+ ///
+ /// Transform a subpath using this transformation matrix.
+ ///
+ /// The original subpath.
+ /// A new subpath which is the result of applying this transformation matrix.
+ public PdfSubpath Transform(PdfSubpath subpath)
+ {
+ var trSubpath = new PdfSubpath();
+ foreach (var c in subpath.Commands)
+ {
+ if (c is Move move)
+ {
+ var loc = Transform(move.Location);
+ trSubpath.MoveTo(loc.X, loc.Y);
+ }
+ else if (c is Line line)
+ {
+ //var from = Transform(line.From);
+ var to = Transform(line.To);
+ trSubpath.LineTo(to.X, to.Y);
+ }
+ else if (c is BezierCurve curve)
+ {
+ var first = Transform(curve.FirstControlPoint);
+ var second = Transform(curve.SecondControlPoint);
+ var end = Transform(curve.EndPoint);
+ trSubpath.BezierCurveTo(first.X, first.Y, second.X, second.Y, end.X, end.Y);
+ }
+ else if (c is Close)
+ {
+ trSubpath.CloseSubpath();
+ }
+ else
+ {
+ throw new Exception("Unknown PdfSubpath type");
+ }
+ }
+ return trSubpath;
+ }
+
+ ///
+ /// Transform a path using this transformation matrix.
+ ///
+ /// The original path.
+ /// A new path which is the result of applying this transformation matrix.
+ public IEnumerable Transform(IEnumerable path)
+ {
+ foreach (var subpath in path)
+ {
+ yield return Transform(subpath);
+ }
+ }
+
///
/// Generate a translated by the specified amount.
///
diff --git a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2BuildCharContext.cs b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2BuildCharContext.cs
index d1af650c..4b5e0272 100644
--- a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2BuildCharContext.cs
+++ b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2BuildCharContext.cs
@@ -1,7 +1,7 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
- using System.Collections.Generic;
using Core;
+ using System.Collections.Generic;
///
/// The context used and updated when interpreting the commands for a charstring.
@@ -18,7 +18,7 @@
///
/// The current path.
///
- public PdfSubpath Path { get; } = new PdfSubpath();
+ public List Path { get; } = new List();
///
/// The current location of the active point.
@@ -41,6 +41,28 @@
AddRelativeLine(0, dy);
}
+ public void AddRelativeMoveTo(double dx, double dy)
+ {
+ BeforeMoveTo();
+ var newLocation = new PdfPoint(CurrentLocation.X + dx, CurrentLocation.Y + dy);
+ Path[Path.Count - 1].MoveTo(newLocation.X, newLocation.Y);
+ CurrentLocation = newLocation;
+ }
+
+ public void AddHorizontalMoveTo(double dx)
+ {
+ BeforeMoveTo();
+ Path[Path.Count - 1].MoveTo(CurrentLocation.X + dx, CurrentLocation.Y);
+ CurrentLocation = CurrentLocation.MoveX(dx);
+ }
+
+ public void AddVerticallMoveTo(double dy)
+ {
+ BeforeMoveTo();
+ Path[Path.Count - 1].MoveTo(CurrentLocation.X, CurrentLocation.Y + dy);
+ CurrentLocation = CurrentLocation.MoveY(dy);
+ }
+
public void AddRelativeBezierCurve(double dx1, double dy1, double dx2, double dy2, double dx3, double dy3)
{
var x1 = CurrentLocation.X + dx1;
@@ -52,7 +74,7 @@
var x3 = x2 + dx3;
var y3 = y2 + dy3;
- Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
+ Path[Path.Count - 1].BezierCurveTo(x1, y1, x2, y2, x3, y3);
CurrentLocation = new PdfPoint(x3, y3);
}
@@ -60,7 +82,7 @@
{
var dest = new PdfPoint(CurrentLocation.X + dx, CurrentLocation.Y + dy);
- Path.LineTo(dest.X, dest.Y);
+ Path[Path.Count - 1].LineTo(dest.X, dest.Y);
CurrentLocation = dest;
}
@@ -77,6 +99,23 @@
transientArray[location] = value;
}
+ ///
+ /// Every character path and subpath must begin with one of the
+ /// moveto operators. If the current path is open when a moveto
+ /// operator is encountered, the path is closed before performing
+ /// the moveto operation.
+ /// See 4.1 Path Construction Operators in 'The Type 2 Charstring Format, Technical Note #5177', 16 March 2000
+ ///
+ ///
+ private void BeforeMoveTo()
+ {
+ if (Path.Count > 0)
+ {
+ Path[Path.Count - 1].CloseSubpath();
+ }
+ Path.Add(new PdfSubpath());
+ }
+
public double GetFromTransientArray(int location)
{
var result = transientArray[location];
diff --git a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStringParser.cs b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStringParser.cs
index b6785ce9..313580b6 100644
--- a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStringParser.cs
+++ b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStringParser.cs
@@ -1,951 +1,936 @@
-// ReSharper disable CompareOfFloatsByEqualityOperator
-namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
-{
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- using System.Runtime.CompilerServices;
- using Charsets;
- using Core;
-
- ///
- /// Decodes the commands and numbers making up a Type 2 CharString. A Type 2 CharString extends on the Type 1 CharString format.
- /// Compared to the Type 1 format, the Type 2 encoding offers smaller size and an opportunity for better rendering quality and
- /// performance. The Type 2 charstring operators are (with one exception) a superset of the Type 1 operators.
- ///
- ///
- /// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators.
- /// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner
- ///
- internal class Type2CharStringParser
- {
- private const byte HstemByte = 1;
- private const byte VstemByte = 3;
- private const byte HstemhmByte = 18;
- private const byte HintmaskByte = 19;
- private const byte CntrmaskByte = 20;
- private const byte VstemhmByte = 23;
-
- private static readonly HashSet HintingCommandBytes = new HashSet
- {
- HstemByte,
- VstemByte,
- HstemhmByte,
- VstemhmByte
- };
-
- private static readonly IReadOnlyDictionary SingleByteCommandStore = new Dictionary
- {
- { HstemByte, new LazyType2Command("hstem", 2, ctx =>
- {
- var numberOfEdgeHints = ctx.Stack.Length / 2;
- var hints = new (double, double)[numberOfEdgeHints];
-
- var firstStartY = ctx.Stack.PopBottom();
- var endY = firstStartY + ctx.Stack.PopBottom();
-
- hints[0] = (firstStartY, endY);
-
- var currentY = endY;
-
- for (var i = 1; i < numberOfEdgeHints; i++)
- {
- var dyStart = ctx.Stack.PopBottom();
- var dyEnd = ctx.Stack.PopBottom();
-
- hints[i] = (currentY + dyStart, currentY + dyStart + dyEnd);
- currentY = currentY + dyStart + dyEnd;
- }
-
- ctx.AddHorizontalStemHints(hints);
-
- ctx.Stack.Clear();
- })
- },
- {
- VstemByte, new LazyType2Command("vstem", 2, ctx =>
- {
- var numberOfEdgeHints = ctx.Stack.Length / 2;
- var hints = new (double, double)[numberOfEdgeHints];
-
- var firstStartX = ctx.Stack.PopBottom();
- var endX = firstStartX + ctx.Stack.PopBottom();
-
- hints[0] = (firstStartX, endX);
-
- var currentX = endX;
-
- for (var i = 1; i < numberOfEdgeHints; i++)
- {
- var dxStart = ctx.Stack.PopBottom();
- var dxEnd = ctx.Stack.PopBottom();
-
- hints[i] = (currentX + dxStart, currentX + dxStart + dxEnd);
- currentX = currentX + dxStart + dxEnd;
- }
-
- ctx.AddVerticalStemHints(hints);
-
- ctx.Stack.Clear();
- })
- },
- { 4,
- new LazyType2Command("vmoveto", 1, ctx =>
- {
- var dy = ctx.Stack.PopBottom();
-
- ctx.Path.MoveTo(ctx.CurrentLocation.X, ctx.CurrentLocation.Y + dy);
- ctx.CurrentLocation = ctx.CurrentLocation.MoveY(dy);
-
- ctx.Stack.Clear();
- })
- },
- { 5,
- new LazyType2Command("rlineto", 2, ctx =>
- {
- var numberOfLines = ctx.Stack.Length / 2;
-
- for (var i = 0; i < numberOfLines; i++)
- {
- var dxa = ctx.Stack.PopBottom();
- var dya = ctx.Stack.PopBottom();
-
- ctx.AddRelativeLine(dxa, dya);
- }
-
- ctx.Stack.Clear();
- })
- },
- { 6,
- new LazyType2Command("hlineto", 1, ctx =>
- {
- /*
- * Appends a horizontal line of length dx1 to the current point.
- * With an odd number of arguments, subsequent argument pairs are interpreted as alternating values of dy and dx.
- * With an even number of arguments, the arguments are interpreted as alternating horizontal and vertical lines (dx and dy).
- * The number of lines is determined from the number of arguments on the stack.
- */
- var isOdd = ctx.Stack.Length % 2 != 0;
-
- var numberOfAdditionalLines = ctx.Stack.Length - (isOdd ? 1 : 0);
-
- if (isOdd)
- {
- var dx1 = ctx.Stack.PopBottom();
- ctx.AddRelativeHorizontalLine(dx1);
-
- for (var i = 0; i < numberOfAdditionalLines; i+= 2)
- {
- ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
- ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
- }
- }
- else
- {
- for (var i = 0; i < numberOfAdditionalLines; i+= 2)
- {
- ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
- ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
- }
- }
-
- ctx.Stack.Clear();
- })
- },
- { 7,
- new LazyType2Command("vlineto", 1, ctx =>
- {
- var isOdd = ctx.Stack.Length % 2 != 0;
-
- var numberOfAdditionalLines = ctx.Stack.Length - (isOdd ? 1 : 0);
-
- if (isOdd)
- {
- var dy1 = ctx.Stack.PopBottom();
- ctx.AddRelativeVerticalLine(dy1);
-
- for (var i = 0; i < numberOfAdditionalLines; i+=2)
- {
- ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
- ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
- }
- }
- else
- {
- for (var i = 0; i < numberOfAdditionalLines; i+=2)
- {
- ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
- ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
- }
- }
-
- ctx.Stack.Clear();
- })
- },
- { 8,
- new LazyType2Command("rrcurveto", 6, ctx =>
- {
- var curveCount = ctx.Stack.Length / 6;
- for (var i = 0; i < curveCount; i++)
- {
- ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom());
- }
-
- ctx.Stack.Clear();
- })
- },
- { 10, new LazyType2Command("callsubr", 1, ctx => {})},
- { 11, new LazyType2Command("return", 0, ctx => {})},
- { 14, new LazyType2Command("endchar", 0, ctx =>
- {
- ctx.Stack.Clear();
- })
- },
- { HstemhmByte, new LazyType2Command("hstemhm", 2, ctx =>
- {
- // Same as vstem except the charstring contains hintmask
- var numberOfEdgeHints = ctx.Stack.Length / 2;
- var hints = new (double, double)[numberOfEdgeHints];
-
- var firstStartY = ctx.Stack.PopBottom();
- var endY = firstStartY + ctx.Stack.PopBottom();
-
- hints[0] = (firstStartY, endY);
-
- var currentY = endY;
-
- for (var i = 1; i < numberOfEdgeHints; i++)
- {
- var dyStart = ctx.Stack.PopBottom();
- var dyEnd = ctx.Stack.PopBottom();
-
- hints[i] = (currentY + dyStart, currentY + dyStart + dyEnd);
- currentY = currentY + dyStart + dyEnd;
- }
-
- ctx.AddHorizontalStemHints(hints);
-
- ctx.Stack.Clear();
- })
- },
- {
- HintmaskByte, new LazyType2Command("hintmask", 0, ctx =>
- {
- // TODO: record this mask somewhere
- ctx.Stack.Clear();
- })
- },
- {
- CntrmaskByte, new LazyType2Command("cntrmask", 0,ctx =>
- {
- // TODO: record this mask somewhere
- ctx.Stack.Clear();
- })
- },
- { 21,
- new LazyType2Command("rmoveto", 2, ctx =>
+// ReSharper disable CompareOfFloatsByEqualityOperator
+namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
+{
+ using Charsets;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Decodes the commands and numbers making up a Type 2 CharString. A Type 2 CharString extends on the Type 1 CharString format.
+ /// Compared to the Type 1 format, the Type 2 encoding offers smaller size and an opportunity for better rendering quality and
+ /// performance. The Type 2 charstring operators are (with one exception) a superset of the Type 1 operators.
+ ///
+ ///
+ /// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators.
+ /// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner
+ ///
+ internal static class Type2CharStringParser
+ {
+ private const byte HstemByte = 1;
+ private const byte VstemByte = 3;
+ private const byte HstemhmByte = 18;
+ private const byte HintmaskByte = 19;
+ private const byte CntrmaskByte = 20;
+ private const byte VstemhmByte = 23;
+
+ private static readonly HashSet HintingCommandBytes = new HashSet
+ {
+ HstemByte,
+ VstemByte,
+ HstemhmByte,
+ VstemhmByte
+ };
+
+ private static readonly IReadOnlyDictionary SingleByteCommandStore = new Dictionary
+ {
+ { HstemByte, new LazyType2Command("hstem", 2, ctx =>
{
- var dx = ctx.Stack.PopBottom();
- var dy = ctx.Stack.PopBottom();
-
- var newLocation = new PdfPoint(ctx.CurrentLocation.X + dx,
- ctx.CurrentLocation.Y + dy);
-
- ctx.Path.MoveTo(newLocation.X, newLocation.Y);
- ctx.CurrentLocation = newLocation;
-
- ctx.Stack.Clear();
- })
- },
- { 22,
- new LazyType2Command("hmoveto", 1, ctx =>
- {
- var dx = ctx.Stack.PopBottom();
-
- ctx.Path.MoveTo(ctx.CurrentLocation.X + dx, ctx.CurrentLocation.Y);
- ctx.CurrentLocation = ctx.CurrentLocation.MoveX(dx);
-
- ctx.Stack.Clear();
- })
- },
- { VstemhmByte, new LazyType2Command("vstemhm", 2, ctx =>
- {
- // Same as vstem except the charstring contains hintmask
- var numberOfEdgeHints = ctx.Stack.Length / 2;
- var hints = new (double, double)[numberOfEdgeHints];
-
- var firstStartX = ctx.Stack.PopBottom();
- var endX = firstStartX + ctx.Stack.PopBottom();
-
- hints[0] = (firstStartX, endX);
-
- var currentX = endX;
-
- for (var i = 1; i < numberOfEdgeHints; i++)
- {
- var dxStart = ctx.Stack.PopBottom();
- var dxEnd = ctx.Stack.PopBottom();
-
- hints[i] = (currentX + dxStart, currentX + dxStart + dxEnd);
- currentX = currentX + dxStart + dxEnd;
- }
-
- ctx.AddVerticalStemHints(hints);
-
- ctx.Stack.Clear();
- })
- },
- {
- 24,
- new LazyType2Command("rcurveline", 8, ctx =>
- {
- var numberOfCurves = (ctx.Stack.Length - 2) / 6;
- for (var i = 0; i < numberOfCurves; i++)
- {
- ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom());
- }
-
- ctx.AddRelativeLine(ctx.Stack.PopBottom(), ctx.Stack.PopBottom());
- ctx.Stack.Clear();
- })
- },
- { 25,
- new LazyType2Command("rlinecurve", 8, ctx =>
- {
- var numberOfLines = (ctx.Stack.Length - 6) / 2;
- for (var i = 0; i < numberOfLines; i++)
- {
- ctx.AddRelativeLine(ctx.Stack.PopBottom(), ctx.Stack.PopBottom());
- }
-
- ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom());
-
- ctx.Stack.Clear();
- })
- },
- { 26,
- new LazyType2Command("vvcurveto", 4, ctx =>
- {
- // dx1? {dya dxb dyb dyc}+
- var hasDeltaXFirstCurve = ctx.Stack.Length % 4 != 0;
-
- var numberOfCurves = ctx.Stack.Length / 4;
- for (var i = 0; i < numberOfCurves; i++)
- {
- var dx1 = 0.0;
- if (i == 0 && hasDeltaXFirstCurve)
- {
- dx1 = ctx.Stack.PopBottom();
- }
-
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
-
- ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, 0, dy3);
- }
-
- ctx.Stack.Clear();
- })
- },
- { 27, new LazyType2Command("hhcurveto", 4, ctx =>
- {
- // dy1? {dxa dxb dyb dxc}+
- var hasDeltaYFirstCurve = ctx.Stack.Length % 4 != 0;
-
- if (hasDeltaYFirstCurve)
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
-
- ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, dx3, 0);
- }
-
- var numberOfCurves = ctx.Stack.Length / 4;
- for (var i = 0; i < numberOfCurves; i++)
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
-
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, 0);
- }
-
- ctx.Stack.Clear();
- })
- },
- { 29, new LazyType2Command("callgsubr", 1, ctx => {})
- },
- { 30,
- new LazyType2Command("vhcurveto", 4, ctx =>
- {
- var remainder = ctx.Stack.Length % 8;
-
- if (remainder <= 1)
- {
- // {dya dxb dyb dxc dxd dxe dye dyf}+ dxf?
- // 2 curves, 1st starts vertical ends horizontal, second starts horizontal ends vertical
-
- var numberOfCurves = (ctx.Stack.Length - remainder)/8;
- for (var i = 0; i < numberOfCurves; i++)
- {
- // First curve
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, 0);
- }
- // Second curve
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
- var dx3 = 0.0;
-
- if (i == numberOfCurves - 1 && remainder == 1)
- {
- dx3 = ctx.Stack.PopBottom();
- }
-
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
- }
- }
- }
- else if (remainder == 4 || remainder == 5)
- {
- // dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf?
- var numberOfCurves = (ctx.Stack.Length - remainder) / 8;
-
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.Length == 1 ? ctx.Stack.PopBottom() : 0;
- ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
- }
-
- for (var i = 0; i < numberOfCurves; i++)
- {
- // First curve
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, 0, dy3);
- }
- // Second curve
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- var dy3 = 0.0;
-
- if (i == numberOfCurves - 1 && remainder == 5)
- {
- dy3 = ctx.Stack.PopBottom();
- }
-
- ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
- }
- }
- }
- else
- {
- throw new InvalidOperationException($"Unexpected number of arguments for vhcurve to: {ctx.Stack.Length}.");
- }
-
- ctx.Stack.Clear();
- })
- },
- { 31,
- new LazyType2Command("hvcurveto", 4, ctx =>
- {
- var remainder = ctx.Stack.Length % 8;
-
- if (remainder <= 1)
- {
- // {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf?
- // 2 curves, 1st starts horizontal ends vertical, second starts vertical ends horizontal
-
- var numberOfCurves = (ctx.Stack.Length - remainder)/8;
- for (var i = 0; i < numberOfCurves; i++)
- {
- // First curve
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, 0, dy3);
- }
- // Second curve
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- var dy3 = 0.0;
-
- if (i == numberOfCurves - 1 && remainder == 1)
- {
- dy3 = ctx.Stack.PopBottom();
- }
-
- ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
- }
- }
- }
- else if (remainder == 4 || remainder == 5)
- {
- // dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf?
- var numberOfCurves = (ctx.Stack.Length - remainder) / 8;
-
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.Length == 1 ? ctx.Stack.PopBottom() : 0;
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
- }
-
- for (var i = 0; i < numberOfCurves; i++)
- {
- // First curve
- {
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, 0);
- }
- // Second curve
- {
- var dx1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
- var dx3 = 0.0;
-
- if (i == numberOfCurves - 1 && remainder == 5)
- {
- dx3 = ctx.Stack.PopBottom();
- }
-
- ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
- }
- }
- }
- else
- {
- throw new InvalidOperationException($"Unexpected number of arguments for hvcurve to: {ctx.Stack.Length}.");
- }
-
- ctx.Stack.Clear();
- })
- },
- { 255, new LazyType2Command("unknown", -1, x => {}) }
+ var numberOfEdgeHints = ctx.Stack.Length / 2;
+ var hints = new (double, double)[numberOfEdgeHints];
+
+ var firstStartY = ctx.Stack.PopBottom();
+ var endY = firstStartY + ctx.Stack.PopBottom();
+
+ hints[0] = (firstStartY, endY);
+
+ var currentY = endY;
+
+ for (var i = 1; i < numberOfEdgeHints; i++)
+ {
+ var dyStart = ctx.Stack.PopBottom();
+ var dyEnd = ctx.Stack.PopBottom();
+
+ hints[i] = (currentY + dyStart, currentY + dyStart + dyEnd);
+ currentY = currentY + dyStart + dyEnd;
+ }
+
+ ctx.AddHorizontalStemHints(hints);
+
+ ctx.Stack.Clear();
+ })
+ },
+ {
+ VstemByte, new LazyType2Command("vstem", 2, ctx =>
+ {
+ var numberOfEdgeHints = ctx.Stack.Length / 2;
+ var hints = new (double, double)[numberOfEdgeHints];
+
+ var firstStartX = ctx.Stack.PopBottom();
+ var endX = firstStartX + ctx.Stack.PopBottom();
+
+ hints[0] = (firstStartX, endX);
+
+ var currentX = endX;
+
+ for (var i = 1; i < numberOfEdgeHints; i++)
+ {
+ var dxStart = ctx.Stack.PopBottom();
+ var dxEnd = ctx.Stack.PopBottom();
+
+ hints[i] = (currentX + dxStart, currentX + dxStart + dxEnd);
+ currentX = currentX + dxStart + dxEnd;
+ }
+
+ ctx.AddVerticalStemHints(hints);
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 4,
+ new LazyType2Command("vmoveto", 1, ctx =>
+ {
+ var dy = ctx.Stack.PopBottom();
+ ctx.AddVerticallMoveTo(dy);
+ ctx.Stack.Clear();
+ })
+ },
+ { 5,
+ new LazyType2Command("rlineto", 2, ctx =>
+ {
+ var numberOfLines = ctx.Stack.Length / 2;
+
+ for (var i = 0; i < numberOfLines; i++)
+ {
+ var dxa = ctx.Stack.PopBottom();
+ var dya = ctx.Stack.PopBottom();
+
+ ctx.AddRelativeLine(dxa, dya);
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 6,
+ new LazyType2Command("hlineto", 1, ctx =>
+ {
+ /*
+ * Appends a horizontal line of length dx1 to the current point.
+ * With an odd number of arguments, subsequent argument pairs are interpreted as alternating values of dy and dx.
+ * With an even number of arguments, the arguments are interpreted as alternating horizontal and vertical lines (dx and dy).
+ * The number of lines is determined from the number of arguments on the stack.
+ */
+ var isOdd = ctx.Stack.Length % 2 != 0;
+
+ var numberOfAdditionalLines = ctx.Stack.Length - (isOdd ? 1 : 0);
+
+ if (isOdd)
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ ctx.AddRelativeHorizontalLine(dx1);
+
+ for (var i = 0; i < numberOfAdditionalLines; i+= 2)
+ {
+ ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
+ ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
+ }
+ }
+ else
+ {
+ for (var i = 0; i < numberOfAdditionalLines; i+= 2)
+ {
+ ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
+ ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
+ }
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 7,
+ new LazyType2Command("vlineto", 1, ctx =>
+ {
+ var isOdd = ctx.Stack.Length % 2 != 0;
+
+ var numberOfAdditionalLines = ctx.Stack.Length - (isOdd ? 1 : 0);
+
+ if (isOdd)
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ ctx.AddRelativeVerticalLine(dy1);
+
+ for (var i = 0; i < numberOfAdditionalLines; i+=2)
+ {
+ ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
+ ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
+ }
+ }
+ else
+ {
+ for (var i = 0; i < numberOfAdditionalLines; i+=2)
+ {
+ ctx.AddRelativeVerticalLine(ctx.Stack.PopBottom());
+ ctx.AddRelativeHorizontalLine(ctx.Stack.PopBottom());
+ }
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 8,
+ new LazyType2Command("rrcurveto", 6, ctx =>
+ {
+ var curveCount = ctx.Stack.Length / 6;
+ for (var i = 0; i < curveCount; i++)
+ {
+ ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom());
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 10, new LazyType2Command("callsubr", 1, ctx => {})},
+ { 11, new LazyType2Command("return", 0, ctx => {})},
+ { 14, new LazyType2Command("endchar", 0, ctx =>
+ {
+ ctx.Stack.Clear();
+ })
+ },
+ { HstemhmByte, new LazyType2Command("hstemhm", 2, ctx =>
+ {
+ // Same as vstem except the charstring contains hintmask
+ var numberOfEdgeHints = ctx.Stack.Length / 2;
+ var hints = new (double, double)[numberOfEdgeHints];
+
+ var firstStartY = ctx.Stack.PopBottom();
+ var endY = firstStartY + ctx.Stack.PopBottom();
+
+ hints[0] = (firstStartY, endY);
+
+ var currentY = endY;
+
+ for (var i = 1; i < numberOfEdgeHints; i++)
+ {
+ var dyStart = ctx.Stack.PopBottom();
+ var dyEnd = ctx.Stack.PopBottom();
+
+ hints[i] = (currentY + dyStart, currentY + dyStart + dyEnd);
+ currentY = currentY + dyStart + dyEnd;
+ }
+
+ ctx.AddHorizontalStemHints(hints);
+
+ ctx.Stack.Clear();
+ })
+ },
+ {
+ HintmaskByte, new LazyType2Command("hintmask", 0, ctx =>
+ {
+ // TODO: record this mask somewhere
+ ctx.Stack.Clear();
+ })
+ },
+ {
+ CntrmaskByte, new LazyType2Command("cntrmask", 0,ctx =>
+ {
+ // TODO: record this mask somewhere
+ ctx.Stack.Clear();
+ })
+ },
+ { 21,
+ new LazyType2Command("rmoveto", 2, ctx =>
+ {
+ var dx = ctx.Stack.PopBottom();
+ var dy = ctx.Stack.PopBottom();
+ ctx.AddRelativeMoveTo(dx,dy);
+ ctx.Stack.Clear();
+ })
+ },
+ { 22,
+ new LazyType2Command("hmoveto", 1, ctx =>
+ {
+ var dx = ctx.Stack.PopBottom();
+ ctx.AddHorizontalMoveTo(dx);
+ ctx.Stack.Clear();
+ })
+ },
+ { VstemhmByte, new LazyType2Command("vstemhm", 2, ctx =>
+ {
+ // Same as vstem except the charstring contains hintmask
+ var numberOfEdgeHints = ctx.Stack.Length / 2;
+ var hints = new (double, double)[numberOfEdgeHints];
+
+ var firstStartX = ctx.Stack.PopBottom();
+ var endX = firstStartX + ctx.Stack.PopBottom();
+
+ hints[0] = (firstStartX, endX);
+
+ var currentX = endX;
+
+ for (var i = 1; i < numberOfEdgeHints; i++)
+ {
+ var dxStart = ctx.Stack.PopBottom();
+ var dxEnd = ctx.Stack.PopBottom();
+
+ hints[i] = (currentX + dxStart, currentX + dxStart + dxEnd);
+ currentX = currentX + dxStart + dxEnd;
+ }
+
+ ctx.AddVerticalStemHints(hints);
+
+ ctx.Stack.Clear();
+ })
+ },
+ {
+ 24,
+ new LazyType2Command("rcurveline", 8, ctx =>
+ {
+ var numberOfCurves = (ctx.Stack.Length - 2) / 6;
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom());
+ }
+
+ ctx.AddRelativeLine(ctx.Stack.PopBottom(), ctx.Stack.PopBottom());
+ ctx.Stack.Clear();
+ })
+ },
+ { 25,
+ new LazyType2Command("rlinecurve", 8, ctx =>
+ {
+ var numberOfLines = (ctx.Stack.Length - 6) / 2;
+ for (var i = 0; i < numberOfLines; i++)
+ {
+ ctx.AddRelativeLine(ctx.Stack.PopBottom(), ctx.Stack.PopBottom());
+ }
+
+ ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom());
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 26,
+ new LazyType2Command("vvcurveto", 4, ctx =>
+ {
+ // dx1? {dya dxb dyb dyc}+
+ var hasDeltaXFirstCurve = ctx.Stack.Length % 4 != 0;
+
+ var numberOfCurves = ctx.Stack.Length / 4;
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ var dx1 = 0.0;
+ if (i == 0 && hasDeltaXFirstCurve)
+ {
+ dx1 = ctx.Stack.PopBottom();
+ }
+
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+
+ ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, 0, dy3);
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 27, new LazyType2Command("hhcurveto", 4, ctx =>
+ {
+ // dy1? {dxa dxb dyb dxc}+
+ var hasDeltaYFirstCurve = ctx.Stack.Length % 4 != 0;
+
+ if (hasDeltaYFirstCurve)
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+
+ ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, dx3, 0);
+ }
+
+ var numberOfCurves = ctx.Stack.Length / 4;
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, 0);
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 29, new LazyType2Command("callgsubr", 1, ctx => {})
+ },
+ { 30,
+ new LazyType2Command("vhcurveto", 4, ctx =>
+ {
+ var remainder = ctx.Stack.Length % 8;
+
+ if (remainder <= 1)
+ {
+ // {dya dxb dyb dxc dxd dxe dye dyf}+ dxf?
+ // 2 curves, 1st starts vertical ends horizontal, second starts horizontal ends vertical
+
+ var numberOfCurves = (ctx.Stack.Length - remainder)/8;
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ // First curve
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, 0);
+ }
+ // Second curve
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+ var dx3 = 0.0;
+
+ if (i == numberOfCurves - 1 && remainder == 1)
+ {
+ dx3 = ctx.Stack.PopBottom();
+ }
+
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
+ }
+ }
+ }
+ else if (remainder == 4 || remainder == 5)
+ {
+ // dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf?
+ var numberOfCurves = (ctx.Stack.Length - remainder) / 8;
+
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.Length == 1 ? ctx.Stack.PopBottom() : 0;
+ ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
+ }
+
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ // First curve
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, 0, dy3);
+ }
+ // Second curve
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ var dy3 = 0.0;
+
+ if (i == numberOfCurves - 1 && remainder == 5)
+ {
+ dy3 = ctx.Stack.PopBottom();
+ }
+
+ ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
+ }
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unexpected number of arguments for vhcurve to: {ctx.Stack.Length}.");
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 31,
+ new LazyType2Command("hvcurveto", 4, ctx =>
+ {
+ var remainder = ctx.Stack.Length % 8;
+
+ if (remainder <= 1)
+ {
+ // {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf?
+ // 2 curves, 1st starts horizontal ends vertical, second starts vertical ends horizontal
+
+ var numberOfCurves = (ctx.Stack.Length - remainder)/8;
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ // First curve
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, 0, dy3);
+ }
+ // Second curve
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ var dy3 = 0.0;
+
+ if (i == numberOfCurves - 1 && remainder == 1)
+ {
+ dy3 = ctx.Stack.PopBottom();
+ }
+
+ ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, dy3);
+ }
+ }
+ }
+ else if (remainder == 4 || remainder == 5)
+ {
+ // dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf?
+ var numberOfCurves = (ctx.Stack.Length - remainder) / 8;
+
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.Length == 1 ? ctx.Stack.PopBottom() : 0;
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
+ }
+
+ for (var i = 0; i < numberOfCurves; i++)
+ {
+ // First curve
+ {
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ ctx.AddRelativeBezierCurve(0, dy1, dx2, dy2, dx3, 0);
+ }
+ // Second curve
+ {
+ var dx1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+ var dx3 = 0.0;
+
+ if (i == numberOfCurves - 1 && remainder == 5)
+ {
+ dx3 = ctx.Stack.PopBottom();
+ }
+
+ ctx.AddRelativeBezierCurve(dx1, 0, dx2, dy2, dx3, dy3);
+ }
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unexpected number of arguments for hvcurve to: {ctx.Stack.Length}.");
+ }
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 255, new LazyType2Command("unknown", -1, x => {}) }
};
- private static readonly IReadOnlyDictionary TwoByteCommandStore = new Dictionary
- {
- { 3, new LazyType2Command("and", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() != 0 && ctx.Stack.PopTop() != 0 ? 1 : 0))},
- { 4, new LazyType2Command("or", 2,ctx =>
- {
- var arg1 = ctx.Stack.PopTop();
- var arg2 = ctx.Stack.PopTop();
- ctx.Stack.Push(arg1 != 0 || arg2 != 0 ? 1 : 0);
- })},
- { 5, new LazyType2Command("not", 1,ctx => ctx.Stack.Push(ctx.Stack.PopTop() == 0 ? 1 : 0))},
- { 9, new LazyType2Command("abs", 1, ctx => ctx.Stack.Push(Math.Abs(ctx.Stack.PopTop())))},
- { 10, new LazyType2Command("add", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() + ctx.Stack.PopTop()))},
- {
- 11, new LazyType2Command("sub", 2, ctx =>
- {
- var num1 = ctx.Stack.PopTop();
- var num2 = ctx.Stack.PopTop();
- ctx.Stack.Push(num2 - num1);
- })
- },
- { 12, new LazyType2Command("div", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop()/ctx.Stack.PopTop()))},
- { 14, new LazyType2Command("neg", 1, ctx => ctx.Stack.Push(-1 * Math.Abs(ctx.Stack.PopTop())))},
- // ReSharper disable once EqualExpressionComparison
- { 15, new LazyType2Command("eq", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() == ctx.Stack.PopTop() ? 1 : 0))},
- { 18, new LazyType2Command("drop", 1, ctx => ctx.Stack.PopTop())},
- { 20, new LazyType2Command("put", 2, ctx => ctx.AddToTransientArray(ctx.Stack.PopTop(), (int)ctx.Stack.PopTop()))},
- { 21, new LazyType2Command("get", 1, ctx => ctx.Stack.Push(ctx.GetFromTransientArray((int)ctx.Stack.PopTop())))},
- { 22, new LazyType2Command("ifelse", 4, x => { })},
- // TODO: Random, do we want to support this?
- { 23, new LazyType2Command("random", 0, ctx => ctx.Stack.Push(0.5))},
- { 24, new LazyType2Command("mul", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() * ctx.Stack.PopTop()))},
- { 26, new LazyType2Command("sqrt", 1, ctx => ctx.Stack.Push(Math.Sqrt(ctx.Stack.PopTop())))},
- {
- 27, new LazyType2Command("dup", 1, ctx =>
- {
- var val = ctx.Stack.PopTop();
- ctx.Stack.Push(val);
- ctx.Stack.Push(val);
- })
- },
- { 28, new LazyType2Command("exch", 2, ctx =>
- {
- var num1 = ctx.Stack.PopTop();
- var num2 = ctx.Stack.PopTop();
- ctx.Stack.Push(num1);
- ctx.Stack.Push(num2);
- })},
- { 29, new LazyType2Command("index", 2, ctx =>
- {
- var index = ctx.Stack.PopTop();
- var val = ctx.Stack.CopyElementAt((int) index);
- ctx.Stack.Push(val);
- })},
- {
- 30, new LazyType2Command("roll", 3, ctx =>
- {
- // TODO: roll
- })
- },
- {
- 34, new LazyType2Command("hflex", 7, ctx =>
- {
- // dx1 dx2 dy2 dx3 dx4 dx5 dx6
- // Two Bezier curves with an fd of 50
-
- // TODO: implement
- ctx.Stack.Clear();
- })
- },
- {
- 35, new LazyType2Command("flex", 13, ctx =>
- {
- // dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd
- // Two Bezier curves will be represented as a straight line when depth less than fd character space units
- ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom());
-
- ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom(),
- ctx.Stack.PopBottom());
-
- ctx.Stack.PopBottom();
- // TODO: record flex depth for this Bezier pair
-
- ctx.Stack.Clear();
- })
- },
- { 36, new LazyType2Command("hflex1", 9, ctx =>
- {
- // TODO: implement
- ctx.Stack.Clear();
- })},
- { 37, new LazyType2Command("flex1", 11, ctx =>
- {
- // dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6
- // d6 is either dx or dy
-
- var dx1 = ctx.Stack.PopBottom();
- var dy1 = ctx.Stack.PopBottom();
- var dx2 = ctx.Stack.PopBottom();
- var dy2 = ctx.Stack.PopBottom();
- var dx3 = ctx.Stack.PopBottom();
- var dy3 = ctx.Stack.PopBottom();
-
- var dx4 = ctx.Stack.PopBottom();
- var dy4 = ctx.Stack.PopBottom();
- var dx5 = ctx.Stack.PopBottom();
- var dy5 = ctx.Stack.PopBottom();
- var d6 = ctx.Stack.PopBottom();
-
- var dx = dx1 + dx2 + dx3 + dx4 + dx5;
- var dy = dy1 + dy2 + dy3 + dy4 + dy5;
-
- var lastPointIsX = Math.Abs(dx) > Math.Abs(dy);
- ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, dx3, dy3);
- ctx.AddRelativeBezierCurve(dx4, dy4, dx5, dy5, lastPointIsX ? d6 : 0, lastPointIsX ? 0 : d6);
- ctx.Stack.Clear();
- })},
- };
-
- public static LazyType2Command GetCommand(Type2CharStrings.CommandSequence.CommandIdentifier identifier)
- {
- if (identifier.IsMultiByteCommand)
- {
- return TwoByteCommandStore[identifier.CommandId];
- }
-
- return SingleByteCommandStore[identifier.CommandId];
- }
-
- public static Type2CharStrings Parse(IReadOnlyList> charStringBytes,
- CompactFontFormatSubroutinesSelector subroutinesSelector, ICompactFontFormatCharset charset)
- {
- if (charStringBytes == null)
- {
- throw new ArgumentNullException(nameof(charStringBytes));
- }
-
- if (subroutinesSelector == null)
- {
- throw new ArgumentNullException(nameof(subroutinesSelector));
- }
-
- var charStrings = new Dictionary();
- for (var i = 0; i < charStringBytes.Count; i++)
- {
- var charString = charStringBytes[i];
- var name = charset.GetNameByGlyphId(i);
- var (globalSubroutines, localSubroutines) = subroutinesSelector.GetSubroutines(i);
- var sequence = ParseSingle(charString.ToList(), localSubroutines, globalSubroutines);
- charStrings[name] = sequence;
- }
-
- return new Type2CharStrings(charStrings);
- }
-
- private static Type2CharStrings.CommandSequence ParseSingle(List bytes,
- CompactFontFormatIndex localSubroutines,
- CompactFontFormatIndex globalSubroutines)
- {
- var values = new List();
- var commandIdentifiers = new List();
-
- for (var i = 0; i < bytes.Count; i++)
- {
- var b = bytes[i];
- if (b <= 31 && b != 28)
- {
- var command = GetCommand(b, bytes,
- values,
- commandIdentifiers,
- localSubroutines,
- globalSubroutines,
- ref i);
-
- if (command != null)
- {
- commandIdentifiers.Add(command.Value);
- }
- }
- else
- {
- var number = InterpretNumber(b, bytes, ref i);
- values.Add(number);
- }
- }
-
- return new Type2CharStrings.CommandSequence(values, commandIdentifiers);
- }
-
- ///
- /// The Type 2 interpretation of a number with an initial byte value of 255 differs from how it is interpreted in the Type 1 format
- /// and 28 has a special meaning.
- ///
- private static float InterpretNumber(byte b, IReadOnlyList bytes, ref int i)
- {
- if (b == 28)
- {
- var num = bytes[++i] << 8 | bytes[++i];
- // Next 2 bytes are a 16-bit two's complement number.
- return (short)(num);
- }
-
- if (b >= 32 && b <= 246)
- {
- return b - 139;
- }
-
- if (b >= 247 && b <= 250)
- {
- var w = bytes[++i];
- return ((b - 247) * 256) + w + 108;
- }
-
- if (b >= 251 && b <= 254)
- {
- var w = bytes[++i];
- return -((b - 251) * 256) - w - 108;
- }
-
- /*
- * If the charstring byte contains the value 255, the next four bytes indicate a two's complement signed number.
- * The first of these the four bytes contains the highest order bits, the second byte contains the next higher order bits
- * and the fourth byte contains the lowest order bits.
- * This number is interpreted as a Fixed; that is, a signed number with 16 bits of fraction
- */
- var lead = (short)(bytes[++i] << 8) + bytes[++i];
- var fractionalPart = (bytes[++i] << 8) + bytes[++i];
-
- return lead + (fractionalPart / 65535.0f);
- }
-
- private static Type2CharStrings.CommandSequence.CommandIdentifier? GetCommand(byte b, List bytes,
- List precedingValues,
- List precedingCommands,
- CompactFontFormatIndex localSubroutines,
- CompactFontFormatIndex globalSubroutines, ref int i)
- {
- const byte returnCommand = 11;
-
- if (b == 12)
- {
- var b2 = bytes[++i];
- if (TwoByteCommandStore.ContainsKey(b2))
- {
- return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, true, b2);
- }
-
- return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, 255);
- }
-
- // Invoke a subroutine, substitute the subroutine bytes into this sequence.
- if (b == 10 || b == 29)
- {
- var isLocal = b == 10;
- int precedingNumber = (int)precedingValues[precedingValues.Count - 1];
-
- var bias = Type2BuildCharContext.CountToBias(isLocal ? localSubroutines.Count : globalSubroutines.Count);
- var index = precedingNumber + bias;
- var subroutineBytes = isLocal ? localSubroutines[index] : globalSubroutines[index];
- bytes.RemoveRange(i - 1, 2);
- bytes.InsertRange(i - 1, subroutineBytes);
-
- // Remove the subroutine index
- precedingValues.RemoveAt(precedingValues.Count - 1);
- i -= 2;
- return null;
- }
-
- if (b == 19 || b == 20)
- {
- // hintmask and cntrmask
- var minimumFullBytes = CalculatePrecedingHintBytes(precedingValues, precedingCommands);
- // Skip the following hintmask or cntrmask data bytes
- i += minimumFullBytes;
- }
-
- if (SingleByteCommandStore.ContainsKey(b))
- {
- // Ignore return
- if (b == returnCommand)
- {
- return null;
- }
-
- return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, b);
- }
-
- return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, 255);
- }
-
- private static int CalculatePrecedingHintBytes(List precedingValues,
- List precedingCommands)
- {
- int SafeStemCount(int counts)
- {
- // Where there an odd number of stem arguments take only as many as the even number requires.
- if (counts % 2 == 0)
- {
- return counts / 2;
- }
-
- return (counts - 1) / 2;
- }
-
- /*
- * The hintmask operator is followed by one or more data bytes that specify the stem hints which are to be active for the
- * subsequent path construction. The number of data bytes must be exactly the number needed to represent the number of
- * stems in the original stem list (those stems specified by the hstem, vstem, hstemhm, or vstemhm commands), using one bit
- * in the data bytes for each stem in the original stem list.
- */
- var stemCount = 0;
- var precedingNumbers = 0;
- var hasEncounteredInitialHintMask = false;
-
- for (var i = -1; i < precedingValues.Count; i++)
- {
- if (i >= 0)
- {
- precedingNumbers++;
- }
-
- for (var j = 0; j < precedingCommands.Count; j++)
- {
- var identifier = precedingCommands[j];
- if (identifier.CommandIndex != i + 1)
- {
- continue;
- }
-
- if (!identifier.IsMultiByteCommand
- && (identifier.CommandId == HintmaskByte || identifier.CommandId == CntrmaskByte)
- && !hasEncounteredInitialHintMask)
- {
- hasEncounteredInitialHintMask = true;
- stemCount += SafeStemCount(precedingNumbers);
- }
- else if (!identifier.IsMultiByteCommand && !HintingCommandBytes.Contains(identifier.CommandId))
- {
- precedingNumbers = 0;
- }
- else if (identifier.IsMultiByteCommand && identifier.CommandId > 35)
- {
- precedingNumbers = 0;
- }
- else
- {
- stemCount += SafeStemCount(precedingNumbers);
- precedingNumbers = 0;
- }
-
- if (hasEncounteredInitialHintMask)
- {
- break;
- }
- }
-
- if (hasEncounteredInitialHintMask)
- {
- break;
- }
- }
-
- var fullStemCount = stemCount;
- // The vstem command can be left out, e.g. for 12 20 hstemhm 4 6 hintmask, 4 and 6 act as the vertical hints
- if (precedingNumbers > 0 && !hasEncounteredInitialHintMask)
- {
- fullStemCount += SafeStemCount(precedingNumbers);
- }
-
- var minimumFullBytes = (int)Math.Ceiling(fullStemCount / 8d);
-
- return minimumFullBytes;
- }
- }
-}
+ private static readonly IReadOnlyDictionary TwoByteCommandStore = new Dictionary
+ {
+ { 3, new LazyType2Command("and", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() != 0 && ctx.Stack.PopTop() != 0 ? 1 : 0))},
+ { 4, new LazyType2Command("or", 2,ctx =>
+ {
+ var arg1 = ctx.Stack.PopTop();
+ var arg2 = ctx.Stack.PopTop();
+ ctx.Stack.Push(arg1 != 0 || arg2 != 0 ? 1 : 0);
+ })},
+ { 5, new LazyType2Command("not", 1,ctx => ctx.Stack.Push(ctx.Stack.PopTop() == 0 ? 1 : 0))},
+ { 9, new LazyType2Command("abs", 1, ctx => ctx.Stack.Push(Math.Abs(ctx.Stack.PopTop())))},
+ { 10, new LazyType2Command("add", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() + ctx.Stack.PopTop()))},
+ {
+ 11, new LazyType2Command("sub", 2, ctx =>
+ {
+ var num1 = ctx.Stack.PopTop();
+ var num2 = ctx.Stack.PopTop();
+ ctx.Stack.Push(num2 - num1);
+ })
+ },
+ { 12, new LazyType2Command("div", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop()/ctx.Stack.PopTop()))},
+ { 14, new LazyType2Command("neg", 1, ctx => ctx.Stack.Push(-1 * Math.Abs(ctx.Stack.PopTop())))},
+ // ReSharper disable once EqualExpressionComparison
+ { 15, new LazyType2Command("eq", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() == ctx.Stack.PopTop() ? 1 : 0))},
+ { 18, new LazyType2Command("drop", 1, ctx => ctx.Stack.PopTop())},
+ { 20, new LazyType2Command("put", 2, ctx => ctx.AddToTransientArray(ctx.Stack.PopTop(), (int)ctx.Stack.PopTop()))},
+ { 21, new LazyType2Command("get", 1, ctx => ctx.Stack.Push(ctx.GetFromTransientArray((int)ctx.Stack.PopTop())))},
+ { 22, new LazyType2Command("ifelse", 4, x => { })},
+ // TODO: Random, do we want to support this?
+ { 23, new LazyType2Command("random", 0, ctx => ctx.Stack.Push(0.5))},
+ { 24, new LazyType2Command("mul", 2, ctx => ctx.Stack.Push(ctx.Stack.PopTop() * ctx.Stack.PopTop()))},
+ { 26, new LazyType2Command("sqrt", 1, ctx => ctx.Stack.Push(Math.Sqrt(ctx.Stack.PopTop())))},
+ {
+ 27, new LazyType2Command("dup", 1, ctx =>
+ {
+ var val = ctx.Stack.PopTop();
+ ctx.Stack.Push(val);
+ ctx.Stack.Push(val);
+ })
+ },
+ { 28, new LazyType2Command("exch", 2, ctx =>
+ {
+ var num1 = ctx.Stack.PopTop();
+ var num2 = ctx.Stack.PopTop();
+ ctx.Stack.Push(num1);
+ ctx.Stack.Push(num2);
+ })},
+ { 29, new LazyType2Command("index", 2, ctx =>
+ {
+ var index = ctx.Stack.PopTop();
+ var val = ctx.Stack.CopyElementAt((int) index);
+ ctx.Stack.Push(val);
+ })},
+ {
+ 30, new LazyType2Command("roll", 3, ctx =>
+ {
+ // TODO: roll
+ })
+ },
+ {
+ 34, new LazyType2Command("hflex", 7, ctx =>
+ {
+ // dx1 dx2 dy2 dx3 dx4 dx5 dx6
+ // Two Bezier curves with an fd of 50
+
+ // TODO: implement
+ ctx.Stack.Clear();
+ })
+ },
+ {
+ 35, new LazyType2Command("flex", 13, ctx =>
+ {
+ // dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd
+ // Two Bezier curves will be represented as a straight line when depth less than fd character space units
+ ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom());
+
+ ctx.AddRelativeBezierCurve(ctx.Stack.PopBottom(), ctx.Stack.PopBottom(), ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom(),
+ ctx.Stack.PopBottom());
+
+ ctx.Stack.PopBottom();
+ // TODO: record flex depth for this Bezier pair
+
+ ctx.Stack.Clear();
+ })
+ },
+ { 36, new LazyType2Command("hflex1", 9, ctx =>
+ {
+ // TODO: implement
+ ctx.Stack.Clear();
+ })},
+ { 37, new LazyType2Command("flex1", 11, ctx =>
+ {
+ // dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6
+ // d6 is either dx or dy
+
+ var dx1 = ctx.Stack.PopBottom();
+ var dy1 = ctx.Stack.PopBottom();
+ var dx2 = ctx.Stack.PopBottom();
+ var dy2 = ctx.Stack.PopBottom();
+ var dx3 = ctx.Stack.PopBottom();
+ var dy3 = ctx.Stack.PopBottom();
+
+ var dx4 = ctx.Stack.PopBottom();
+ var dy4 = ctx.Stack.PopBottom();
+ var dx5 = ctx.Stack.PopBottom();
+ var dy5 = ctx.Stack.PopBottom();
+ var d6 = ctx.Stack.PopBottom();
+
+ var dx = dx1 + dx2 + dx3 + dx4 + dx5;
+ var dy = dy1 + dy2 + dy3 + dy4 + dy5;
+
+ var lastPointIsX = Math.Abs(dx) > Math.Abs(dy);
+ ctx.AddRelativeBezierCurve(dx1, dy1, dx2, dy2, dx3, dy3);
+ ctx.AddRelativeBezierCurve(dx4, dy4, dx5, dy5, lastPointIsX ? d6 : 0, lastPointIsX ? 0 : d6);
+ ctx.Stack.Clear();
+ })},
+ };
+
+ public static LazyType2Command GetCommand(Type2CharStrings.CommandSequence.CommandIdentifier identifier)
+ {
+ if (identifier.IsMultiByteCommand)
+ {
+ return TwoByteCommandStore[identifier.CommandId];
+ }
+
+ return SingleByteCommandStore[identifier.CommandId];
+ }
+
+ public static Type2CharStrings Parse(IReadOnlyList> charStringBytes,
+ CompactFontFormatSubroutinesSelector subroutinesSelector, ICompactFontFormatCharset charset)
+ {
+ if (charStringBytes == null)
+ {
+ throw new ArgumentNullException(nameof(charStringBytes));
+ }
+
+ if (subroutinesSelector == null)
+ {
+ throw new ArgumentNullException(nameof(subroutinesSelector));
+ }
+
+ var charStrings = new Dictionary();
+ for (var i = 0; i < charStringBytes.Count; i++)
+ {
+ var charString = charStringBytes[i];
+ var name = charset.GetNameByGlyphId(i);
+ var (globalSubroutines, localSubroutines) = subroutinesSelector.GetSubroutines(i);
+ var sequence = ParseSingle(charString.ToList(), localSubroutines, globalSubroutines);
+ charStrings[name] = sequence;
+ }
+
+ return new Type2CharStrings(charStrings);
+ }
+
+ private static Type2CharStrings.CommandSequence ParseSingle(List bytes,
+ CompactFontFormatIndex localSubroutines,
+ CompactFontFormatIndex globalSubroutines)
+ {
+ var values = new List();
+ var commandIdentifiers = new List();
+
+ for (var i = 0; i < bytes.Count; i++)
+ {
+ var b = bytes[i];
+ if (b <= 31 && b != 28)
+ {
+ var command = GetCommand(b, bytes,
+ values,
+ commandIdentifiers,
+ localSubroutines,
+ globalSubroutines,
+ ref i);
+
+ if (command != null)
+ {
+ commandIdentifiers.Add(command.Value);
+ }
+ }
+ else
+ {
+ var number = InterpretNumber(b, bytes, ref i);
+ values.Add(number);
+ }
+ }
+
+ return new Type2CharStrings.CommandSequence(values, commandIdentifiers);
+ }
+
+ ///
+ /// The Type 2 interpretation of a number with an initial byte value of 255 differs from how it is interpreted in the Type 1 format
+ /// and 28 has a special meaning.
+ ///
+ private static float InterpretNumber(byte b, IReadOnlyList bytes, ref int i)
+ {
+ if (b == 28)
+ {
+ var num = bytes[++i] << 8 | bytes[++i];
+ // Next 2 bytes are a 16-bit two's complement number.
+ return (short)(num);
+ }
+
+ if (b >= 32 && b <= 246)
+ {
+ return b - 139;
+ }
+
+ if (b >= 247 && b <= 250)
+ {
+ var w = bytes[++i];
+ return ((b - 247) * 256) + w + 108;
+ }
+
+ if (b >= 251 && b <= 254)
+ {
+ var w = bytes[++i];
+ return -((b - 251) * 256) - w - 108;
+ }
+
+ /*
+ * If the charstring byte contains the value 255, the next four bytes indicate a two's complement signed number.
+ * The first of these the four bytes contains the highest order bits, the second byte contains the next higher order bits
+ * and the fourth byte contains the lowest order bits.
+ * This number is interpreted as a Fixed; that is, a signed number with 16 bits of fraction
+ */
+ var lead = (short)(bytes[++i] << 8) + bytes[++i];
+ var fractionalPart = (bytes[++i] << 8) + bytes[++i];
+
+ return lead + (fractionalPart / 65535.0f);
+ }
+
+ private static Type2CharStrings.CommandSequence.CommandIdentifier? GetCommand(byte b, List bytes,
+ List precedingValues,
+ List precedingCommands,
+ CompactFontFormatIndex localSubroutines,
+ CompactFontFormatIndex globalSubroutines, ref int i)
+ {
+ const byte returnCommand = 11;
+
+ if (b == 12)
+ {
+ var b2 = bytes[++i];
+ if (TwoByteCommandStore.ContainsKey(b2))
+ {
+ return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, true, b2);
+ }
+
+ return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, 255);
+ }
+
+ // Invoke a subroutine, substitute the subroutine bytes into this sequence.
+ if (b == 10 || b == 29)
+ {
+ var isLocal = b == 10;
+ int precedingNumber = (int)precedingValues[precedingValues.Count - 1];
+
+ var bias = Type2BuildCharContext.CountToBias(isLocal ? localSubroutines.Count : globalSubroutines.Count);
+ var index = precedingNumber + bias;
+ var subroutineBytes = isLocal ? localSubroutines[index] : globalSubroutines[index];
+ bytes.RemoveRange(i - 1, 2);
+ bytes.InsertRange(i - 1, subroutineBytes);
+
+ // Remove the subroutine index
+ precedingValues.RemoveAt(precedingValues.Count - 1);
+ i -= 2;
+ return null;
+ }
+
+ if (b == 19 || b == 20)
+ {
+ // hintmask and cntrmask
+ var minimumFullBytes = CalculatePrecedingHintBytes(precedingValues, precedingCommands);
+ // Skip the following hintmask or cntrmask data bytes
+ i += minimumFullBytes;
+ }
+
+ if (SingleByteCommandStore.ContainsKey(b))
+ {
+ // Ignore return
+ if (b == returnCommand)
+ {
+ return null;
+ }
+
+ return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, b);
+ }
+
+ return new Type2CharStrings.CommandSequence.CommandIdentifier(precedingValues.Count, false, 255);
+ }
+
+ private static int CalculatePrecedingHintBytes(List precedingValues,
+ List precedingCommands)
+ {
+ int SafeStemCount(int counts)
+ {
+ // Where there an odd number of stem arguments take only as many as the even number requires.
+ if (counts % 2 == 0)
+ {
+ return counts / 2;
+ }
+
+ return (counts - 1) / 2;
+ }
+
+ /*
+ * The hintmask operator is followed by one or more data bytes that specify the stem hints which are to be active for the
+ * subsequent path construction. The number of data bytes must be exactly the number needed to represent the number of
+ * stems in the original stem list (those stems specified by the hstem, vstem, hstemhm, or vstemhm commands), using one bit
+ * in the data bytes for each stem in the original stem list.
+ */
+ var stemCount = 0;
+ var precedingNumbers = 0;
+ var hasEncounteredInitialHintMask = false;
+
+ for (var i = -1; i < precedingValues.Count; i++)
+ {
+ if (i >= 0)
+ {
+ precedingNumbers++;
+ }
+
+ for (var j = 0; j < precedingCommands.Count; j++)
+ {
+ var identifier = precedingCommands[j];
+ if (identifier.CommandIndex != i + 1)
+ {
+ continue;
+ }
+
+ if (!identifier.IsMultiByteCommand
+ && (identifier.CommandId == HintmaskByte || identifier.CommandId == CntrmaskByte)
+ && !hasEncounteredInitialHintMask)
+ {
+ hasEncounteredInitialHintMask = true;
+ stemCount += SafeStemCount(precedingNumbers);
+ }
+ else if (!identifier.IsMultiByteCommand && !HintingCommandBytes.Contains(identifier.CommandId))
+ {
+ precedingNumbers = 0;
+ }
+ else if (identifier.IsMultiByteCommand && identifier.CommandId > 35)
+ {
+ precedingNumbers = 0;
+ }
+ else
+ {
+ stemCount += SafeStemCount(precedingNumbers);
+ precedingNumbers = 0;
+ }
+
+ if (hasEncounteredInitialHintMask)
+ {
+ break;
+ }
+ }
+
+ if (hasEncounteredInitialHintMask)
+ {
+ break;
+ }
+ }
+
+ var fullStemCount = stemCount;
+ // The vstem command can be left out, e.g. for 12 20 hstemhm 4 6 hintmask, 4 and 6 act as the vertical hints
+ if (precedingNumbers > 0 && !hasEncounteredInitialHintMask)
+ {
+ fullStemCount += SafeStemCount(precedingNumbers);
+ }
+
+ var minimumFullBytes = (int)Math.Ceiling(fullStemCount / 8d);
+
+ return minimumFullBytes;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStrings.cs b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStrings.cs
index f60b409b..5ef36ee7 100644
--- a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStrings.cs
+++ b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CharStrings/Type2CharStrings.cs
@@ -1,10 +1,10 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
+ using Core;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
- using Core;
///
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
@@ -21,7 +21,6 @@
///
public IReadOnlyDictionary CharStrings { get; }
-
public Type2CharStrings(IReadOnlyDictionary charStrings)
{
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
@@ -70,7 +69,7 @@
private static Type2Glyph Run(CommandSequence sequence, double defaultWidthX, double nominalWidthX)
{
var context = new Type2BuildCharContext();
-
+
var hasRunStackClearingCommand = false;
for (var i = -1; i < sequence.Values.Count; i++)
{
@@ -224,7 +223,7 @@
///
/// The path of the glyph.
///
- public PdfSubpath Path { get; }
+ public IReadOnlyList Path { get; }
///
/// The width of the glyph as a difference from the nominal width X for the font. Optional.
@@ -234,7 +233,7 @@
///
/// Create a new .
///
- public Type2Glyph(PdfSubpath path, double? width)
+ public Type2Glyph(IReadOnlyList path, double? width)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Width = width;
diff --git a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CompactFontFormatFont.cs b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CompactFontFormatFont.cs
index f7f4d81e..b2cf357b 100644
--- a/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CompactFontFormatFont.cs
+++ b/src/UglyToad.PdfPig.Fonts/CompactFontFormat/CompactFontFormatFont.cs
@@ -1,12 +1,12 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
- using System;
- using System.Collections.Generic;
using Charsets;
using CharStrings;
using Core;
using Dictionaries;
using Encodings;
+ using System;
+ using System.Collections.Generic;
using Type1.CharStrings;
///
@@ -69,7 +69,7 @@
}
var glyph = type2CharStrings.Generate(characterName, (double)defaultWidthX, (double)nominalWidthX);
- var rectangle = glyph.Path.GetBoundingRectangle();
+ var rectangle = PdfSubpath.GetBoundingRectangle(glyph.Path);
if (rectangle.HasValue)
{
return rectangle;
@@ -77,7 +77,55 @@
var defaultBoundingBox = TopDictionary.FontBoundingBox;
return new PdfRectangle(0, 0, glyph.Width.GetValueOrDefault(), defaultBoundingBox.Height);
+ }
+ ///
+ /// Get the pdfpath for the character with the given name.
+ ///
+ ///
+ ///
+ ///
+ public bool TryGetPath(string characterName, out IReadOnlyList path)
+ {
+ var defaultWidthX = GetDefaultWidthX(characterName);
+ var nominalWidthX = GetNominalWidthX(characterName);
+
+ if (CharStrings.TryGetFirst(out var _))
+ {
+ throw new NotImplementedException("Type 1 CharStrings in a CFF font are currently unsupported.");
+ }
+
+ if (!CharStrings.TryGetSecond(out var type2CharStrings))
+ {
+ path = null;
+ return false;
+ }
+
+ path = type2CharStrings.Generate(characterName, (double)defaultWidthX, (double)nominalWidthX).Path;
+ return true;
+ }
+
+ ///
+ /// GetCharacterPath
+ ///
+ ///
+ ///
+ public IReadOnlyList GetCharacterPath(string characterName)
+ {
+ var defaultWidthX = GetDefaultWidthX(characterName);
+ var nominalWidthX = GetNominalWidthX(characterName);
+
+ if (CharStrings.TryGetFirst(out var _))
+ {
+ throw new NotImplementedException("Type 1 CharStrings in a CFF font are currently unsupported.");
+ }
+
+ if (!CharStrings.TryGetSecond(out var type2CharStrings))
+ {
+ return null;
+ }
+
+ return type2CharStrings.Generate(characterName, (double)defaultWidthX, (double)nominalWidthX).Path;
}
///
diff --git a/src/UglyToad.PdfPig.Fonts/GlyphList.cs b/src/UglyToad.PdfPig.Fonts/GlyphList.cs
index 885271d2..3871f078 100644
--- a/src/UglyToad.PdfPig.Fonts/GlyphList.cs
+++ b/src/UglyToad.PdfPig.Fonts/GlyphList.cs
@@ -15,7 +15,7 @@
private readonly IReadOnlyDictionary nameToUnicode;
private readonly IReadOnlyDictionary unicodeToName;
-
+
private readonly Dictionary oddNameToUnicodeCache = new Dictionary();
private static readonly Lazy LazyAdobeGlyphList = new Lazy(() => GlyphListFactory.Get("glyphlist"));
@@ -138,9 +138,9 @@
else if (name.StartsWith("u") && name.Length == 5)
{
// test for an alternate Unicode name representation uXXXX
- var codePoint = int.Parse(name.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var codePoint = int.Parse(name.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
- if (codePoint > 0xD7FF && codePoint < 0xE000)
+ if (codePoint > 0xD7FF && codePoint < 0xE000)
{
throw new InvalidFontFormatException(
$"Unicode character name with disallowed code area: {name}");
@@ -159,4 +159,3 @@
}
}
}
-
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/Glyph.cs b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/Glyph.cs
index b8812d77..e9dc16d2 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/Glyph.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/Glyph.cs
@@ -1,7 +1,8 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Glyphs
{
- using System;
using Core;
+ using System;
+ using System.Collections.Generic;
internal class Glyph : IGlyphDescription
{
@@ -111,12 +112,112 @@
scaled = matrix.Translate(scaled);
- newPoints[i] = new GlyphPoint((short)scaled.X, (short)scaled.Y, point.IsOnCurve);
+ newPoints[i] = new GlyphPoint((short)scaled.X, (short)scaled.Y, point.IsOnCurve, point.IsEndOfContour);
}
return new Glyph(IsSimple, Instructions, EndPointsOfContours, newPoints, Bounds);
}
+ #region Subpaths
+ public bool TryGetGlyphPath(out IReadOnlyList subpaths)
+ {
+ subpaths = EmptyArray.Instance;
+ if (Points == null)
+ {
+ return false;
+ }
+
+ if (Points.Length > 0)
+ {
+ subpaths = CalculatePath(Points);
+ }
+ return true;
+ }
+
+ private static IReadOnlyList CalculatePath(GlyphPoint[] points)
+ {
+ // https://github.com/apache/pdfbox/blob/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphRenderer.java
+ var path = new List();
+
+ int start = 0;
+ for (int p = 0; p < points.Length; ++p)
+ {
+ if (points[p].IsEndOfContour)
+ {
+ PdfSubpath subpath = new PdfSubpath();
+ GlyphPoint firstPoint = points[start];
+ GlyphPoint lastPoint = points[p];
+ var contour = new List();
+
+ for (int q = start; q <= p; ++q)
+ {
+ contour.Add(points[q]);
+ }
+
+ if (points[start].IsOnCurve)
+ {
+ // using start point at the contour end
+ contour.Add(firstPoint);
+ }
+ else if (points[p].IsOnCurve)
+ {
+ // first is off-curve point, trying to use one from the end
+ contour.Insert(0, lastPoint);
+ }
+ else
+ {
+ // start and end are off-curve points, creating implicit one
+ var pmid = midValue(firstPoint, lastPoint);
+ contour.Insert(0, pmid);
+ contour.Add(pmid);
+ }
+
+ subpath.MoveTo(contour[0].X, contour[0].Y);
+ for (int j = 1; j < contour.Count; j++)
+ {
+ GlyphPoint pNow = contour[j];
+ if (pNow.IsOnCurve)
+ {
+ subpath.LineTo(pNow.X, pNow.Y);
+ }
+ else if (contour[j + 1].IsOnCurve)
+ {
+ var pPrevious = contour[j - 1];
+ var pNext = contour[j + 1];
+ subpath.BezierCurveTo(pPrevious.X, pPrevious.Y, pNow.X, pNow.Y, pNext.X, pNext.Y);
+ ++j;
+ }
+ else
+ {
+ var pPrevious = contour[j - 1];
+ var pmid = midValue(pNow, contour[j + 1]);
+ subpath.BezierCurveTo(pPrevious.X, pPrevious.Y, pNow.X, pNow.Y, pmid.X, pmid.Y);
+ }
+ }
+ subpath.CloseSubpath();
+ path.Add(subpath);
+ start = p + 1;
+ }
+ }
+
+ return path;
+ }
+
+ private static short midValue(short a, short b)
+ {
+ return (short)(a + (b - a) / 2);
+ }
+
+ ///
+ /// This creates an onCurve point that is between point1 and point2.
+ ///
+ private static GlyphPoint midValue(GlyphPoint point1, GlyphPoint point2)
+ {
+ // this constructs an on-curve, non-endofcountour point
+ return new GlyphPoint(midValue(point1.X, point2.X), midValue(point1.Y, point2.Y), true, false);
+ }
+ #endregion
+
public override string ToString()
{
var type = IsSimple ? "S" : "C";
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/GlyphPoint.cs b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/GlyphPoint.cs
index c693dfef..7d12eba8 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/GlyphPoint.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/GlyphPoint.cs
@@ -1,5 +1,7 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Glyphs
{
+ using UglyToad.PdfPig.Core;
+
internal struct GlyphPoint
{
public short X { get; }
@@ -8,16 +10,19 @@
public bool IsOnCurve { get; }
- public GlyphPoint(short x, short y, bool isOnCurve)
+ public bool IsEndOfContour { get; }
+
+ public GlyphPoint(short x, short y, bool isOnCurve, bool isEndOfContour)
{
X = x;
Y = y;
IsOnCurve = isOnCurve;
+ IsEndOfContour = isEndOfContour;
}
public override string ToString()
{
- return $"({X}, {Y}) | {IsOnCurve}";
+ return $"({X}, {Y}) | {IsOnCurve} | {IsEndOfContour}";
}
}
}
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/IGlyphDescription.cs b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/IGlyphDescription.cs
index 9cfdbe05..00e417f2 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/IGlyphDescription.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/Glyphs/IGlyphDescription.cs
@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Fonts.TrueType.Glyphs
{
using Core;
+ using System.Collections.Generic;
internal interface IGlyphDescription : IMergeableGlyph, ITransformableGlyph
{
@@ -16,6 +17,8 @@
bool IsEmpty { get; }
+ bool TryGetGlyphPath(out IReadOnlyList subpaths);
+
IGlyphDescription DeepClone();
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/Parser/TableRegister.cs b/src/UglyToad.PdfPig.Fonts/TrueType/Parser/TableRegister.cs
index 31cd9025..a695edf9 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/Parser/TableRegister.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/Parser/TableRegister.cs
@@ -9,7 +9,7 @@
public class TableRegister
{
///
- /// This table contains global information about the font.
+ /// This table contains global information about the font.
///
public HeaderTable HeaderTable { get; }
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/Tables/GlyphDataTable.cs b/src/UglyToad.PdfPig.Fonts/TrueType/Tables/GlyphDataTable.cs
index 822a636b..dab2003a 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/Tables/GlyphDataTable.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/Tables/GlyphDataTable.cs
@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
+ using System.Linq;
using Core;
using Glyphs;
using Parser;
@@ -25,9 +26,9 @@
private readonly Lazy> glyphs;
public IReadOnlyList Glyphs => glyphs.Value;
-
- public GlyphDataTable(TrueTypeHeaderTable directoryTable, IReadOnlyList glyphOffsets,
- PdfRectangle maxGlyphBounds,
+
+ public GlyphDataTable(TrueTypeHeaderTable directoryTable, IReadOnlyList glyphOffsets,
+ PdfRectangle maxGlyphBounds,
TrueTypeDataBytes tableBytes)
{
this.glyphOffsets = glyphOffsets;
@@ -69,7 +70,7 @@
bounds = new PdfRectangle(0, 0, 0, 0);
return true;
}
-
+
tableBytes.Seek(offset);
// ReSharper disable once UnusedVariable
@@ -91,7 +92,7 @@
var bytes = data.ReadByteArray((int)table.Length);
- return new GlyphDataTable(table, tableRegister.IndexToLocationTable.GlyphOffsets,
+ return new GlyphDataTable(table, tableRegister.IndexToLocationTable.GlyphOffsets,
tableRegister.HeaderTable.Bounds,
new TrueTypeDataBytes(bytes));
}
@@ -164,7 +165,7 @@
if (contourCount == 0)
{
return new Glyph(true, EmptyArray.Instance, EmptyArray.Instance,
- EmptyArray.Instance,
+ EmptyArray.Instance,
new PdfRectangle(0, 0, 0, 0));
}
@@ -188,11 +189,25 @@
var yCoordinates = ReadCoordinates(data, pointCount, flags, SimpleGlyphFlags.YSingleByte,
SimpleGlyphFlags.ThisYIsTheSame);
+ int endPtIndex = endPointsOfContours.Length - 1;
+ int endPtOfContourIndex = -1;
var points = new GlyphPoint[xCoordinates.Length];
+
for (var i = xCoordinates.Length - 1; i >= 0; i--)
{
+ if (endPtOfContourIndex == -1)
+ {
+ endPtOfContourIndex = endPointsOfContours[endPtIndex];
+ }
+ bool endPt = endPtOfContourIndex == i;
+ if (endPt && endPtIndex > 0)
+ {
+ endPtIndex--;
+ endPtOfContourIndex = -1;
+ }
+
var isOnCurve = (flags[i] & SimpleGlyphFlags.OnCurve) == SimpleGlyphFlags.OnCurve;
- points[i] = new GlyphPoint(xCoordinates[i], yCoordinates[i], isOnCurve);
+ points[i] = new GlyphPoint(xCoordinates[i], yCoordinates[i], isOnCurve, endPt);
}
return new Glyph(true, instructions, endPointsOfContours, points, bounds);
@@ -209,12 +224,12 @@
data.Seek(compositeLocation.Position);
var components = new List();
-
+
// First recursively find all components and ensure they are available.
CompositeGlyphFlags flags;
do
{
- flags = (CompositeGlyphFlags) data.ReadUnsignedShort();
+ flags = (CompositeGlyphFlags)data.ReadUnsignedShort();
var glyphIndex = data.ReadUnsignedShort();
var childGlyph = glyphs[glyphIndex];
@@ -232,7 +247,7 @@
glyphs[glyphIndex] = childGlyph;
}
-
+
short arg1, arg2;
if (HasFlag(flags, CompositeGlyphFlags.Args1And2AreWords))
{
@@ -276,7 +291,6 @@
{
// TODO: Not implemented, it is unclear how to do this.
}
-
} while (HasFlag(flags, CompositeGlyphFlags.MoreComponents));
// Now build the final glyph from the components.
@@ -381,12 +395,12 @@
/// Stores the position after reading the contour count and bounds.
///
public long Position { get; }
-
+
public PdfRectangle Bounds { get; }
-
+
public TemporaryCompositeLocation(long position, PdfRectangle bounds, short contourCount)
{
- if (contourCount >= 0 )
+ if (contourCount >= 0)
{
throw new ArgumentException($"A composite glyph should not have a positive contour count. Got: {contourCount}.", nameof(contourCount));
}
diff --git a/src/UglyToad.PdfPig.Fonts/TrueType/TrueTypeFont.cs b/src/UglyToad.PdfPig.Fonts/TrueType/TrueTypeFont.cs
index 3db9d22f..81004d24 100644
--- a/src/UglyToad.PdfPig.Fonts/TrueType/TrueTypeFont.cs
+++ b/src/UglyToad.PdfPig.Fonts/TrueType/TrueTypeFont.cs
@@ -112,7 +112,7 @@
{
return false;
}
-
+
if (!TableRegister.GlyphTable.TryGetGlyphBounds(index, out boundingBox))
{
return false;
@@ -126,6 +126,28 @@
return true;
}
+ ///
+ /// Try to get the bounding box for a glyph representing the specified character code if present.
+ ///
+ public bool TryGetPath(int characterCode, out IReadOnlyList path) => TryGetPath(characterCode, null, out path);
+
+ ///
+ /// Try to get the path for a glyph representing the specified character code if present.
+ /// Uses a custom mapping of character code to glyph index.
+ ///
+ public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path)
+ {
+ path = EmptyArray.Instance;
+
+ if (!TryGetGlyphIndex(characterCode, characterCodeToGlyphId, out var index)
+ || TableRegister.GlyphTable == null)
+ {
+ return false;
+ }
+
+ return TableRegister.GlyphTable.Glyphs[index].TryGetGlyphPath(out path);
+ }
+
///
/// Try to get the advance width for a glyph representing the specified character code if present.
///
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/ClosePathCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/ClosePathCommand.cs
index b48f3411..a64571fe 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/ClosePathCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/ClosePathCommand.cs
@@ -17,7 +17,7 @@
public static void Run(Type1BuildCharContext context)
{
- context.Path.CloseSubpath();
+ context.Path[context.Path.Count - 1].CloseSubpath();
context.Stack.Clear();
}
}
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HLineToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HLineToCommand.cs
index 7d947214..6eeffda2 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HLineToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HLineToCommand.cs
@@ -22,7 +22,7 @@
var deltaX = context.Stack.PopBottom();
var x = context.CurrentPosition.X + deltaX;
- context.Path.LineTo(x, context.CurrentPosition.Y);
+ context.Path[context.Path.Count - 1].LineTo(x, context.CurrentPosition.Y);
context.CurrentPosition = new PdfPoint(x, context.CurrentPosition.Y);
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HMoveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HMoveToCommand.cs
index 0c415f81..3e01a284 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HMoveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HMoveToCommand.cs
@@ -31,7 +31,9 @@
var x = context.CurrentPosition.X + deltaX;
var y = context.CurrentPosition.Y;
context.CurrentPosition = new PdfPoint(x, y);
- context.Path.MoveTo(x, y);
+
+ context.Path.Add(new PdfSubpath());
+ context.Path[context.Path.Count - 1].MoveTo(x, y);
}
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HvCurveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HvCurveToCommand.cs
index fb4b42cc..93197204 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HvCurveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/HvCurveToCommand.cs
@@ -34,7 +34,7 @@
var x3 = x2;
var y3 = y2 + dy3;
- context.Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
+ context.Path[context.Path.Count - 1].BezierCurveTo(x1, y1, x2, y2, x3, y3);
context.CurrentPosition = new PdfPoint(x3, y3);
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RLineToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RLineToCommand.cs
index 1b20f78e..31d485f0 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RLineToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RLineToCommand.cs
@@ -25,7 +25,7 @@
var x = context.CurrentPosition.X + deltaX;
var y = context.CurrentPosition.Y + deltaY;
- context.Path.LineTo(x, y);
+ context.Path[context.Path.Count - 1].LineTo(x, y);
context.CurrentPosition = new PdfPoint(x, y);
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RMoveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RMoveToCommand.cs
index 8d4efcd0..ca444f93 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RMoveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RMoveToCommand.cs
@@ -37,7 +37,9 @@
var x = context.CurrentPosition.X + deltaX;
var y = context.CurrentPosition.Y + deltaY;
context.CurrentPosition = new PdfPoint(x, y);
- context.Path.MoveTo(x, y);
+
+ context.Path.Add(new PdfSubpath());
+ context.Path[context.Path.Count - 1].MoveTo(x, y);
}
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RelativeRCurveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RelativeRCurveToCommand.cs
index a747419d..4b85eb9b 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RelativeRCurveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/RelativeRCurveToCommand.cs
@@ -37,7 +37,7 @@
var x3 = x2 + dx3;
var y3 = y2 + dy3;
- context.Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
+ context.Path[context.Path.Count - 1].BezierCurveTo(x1, y1, x2, y2, x3, y3);
context.CurrentPosition = new PdfPoint(x3, y3);
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VLineToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VLineToCommand.cs
index 6ecaab4a..2727a743 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VLineToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VLineToCommand.cs
@@ -22,7 +22,7 @@
var deltaY = context.Stack.PopBottom();
var y = context.CurrentPosition.Y + deltaY;
- context.Path.LineTo(context.CurrentPosition.X, y);
+ context.Path[context.Path.Count - 1].LineTo(context.CurrentPosition.X, y);
context.CurrentPosition = new PdfPoint(context.CurrentPosition.X, y);
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VMoveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VMoveToCommand.cs
index f7a9beb6..f4cbb42c 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VMoveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VMoveToCommand.cs
@@ -33,7 +33,9 @@
var x = context.CurrentPosition.X;
context.CurrentPosition = new PdfPoint(x, y);
- context.Path.MoveTo(x, y);
+
+ context.Path.Add(new PdfSubpath());
+ context.Path[context.Path.Count - 1].MoveTo(x, y);
}
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VhCurveToCommand.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VhCurveToCommand.cs
index 5cab25ed..e6f7e84f 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VhCurveToCommand.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/PathConstruction/VhCurveToCommand.cs
@@ -35,7 +35,7 @@
var x3 = x2 + dx3;
var y3 = y2;
- context.Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
+ context.Path[context.Path.Count - 1].BezierCurveTo(x1, y1, x2, y2, x3, y3);
context.CurrentPosition = new PdfPoint(x3, y3);
context.Stack.Clear();
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/Type1BuildCharContext.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/Type1BuildCharContext.cs
index ff422bd4..74e5f3f2 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/Type1BuildCharContext.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Commands/Type1BuildCharContext.cs
@@ -2,12 +2,13 @@
{
using System;
using System.Collections.Generic;
+ using System.Linq;
using Core;
internal class Type1BuildCharContext
{
- private readonly Func characterByIndexFactory;
- private readonly Func characterByNameFactory;
+ private readonly Func> characterByIndexFactory;
+ private readonly Func> characterByNameFactory;
public IReadOnlyDictionary Subroutines { get; }
public double WidthX { get; set; }
@@ -20,7 +21,7 @@
public bool IsFlexing { get; set; }
- public PdfSubpath Path { get; private set; } = new PdfSubpath();
+ public List Path { get; private set; } = new List();
public PdfPoint CurrentPosition { get; set; }
@@ -31,8 +32,8 @@
public List FlexPoints { get; } = new List();
public Type1BuildCharContext(IReadOnlyDictionary subroutines,
- Func characterByIndexFactory,
- Func characterByNameFactory)
+ Func> characterByIndexFactory,
+ Func> characterByNameFactory)
{
this.characterByIndexFactory = characterByIndexFactory ?? throw new ArgumentNullException(nameof(characterByIndexFactory));
this.characterByNameFactory = characterByNameFactory ?? throw new ArgumentNullException(nameof(characterByNameFactory));
@@ -44,19 +45,19 @@
FlexPoints.Add(point);
}
- public PdfSubpath GetCharacter(int characterCode)
+ public IReadOnlyList GetCharacter(int characterCode)
{
return characterByIndexFactory(characterCode);
}
- public PdfSubpath GetCharacter(string characterName)
+ public IReadOnlyList GetCharacter(string characterName)
{
return characterByNameFactory(characterName);
}
- public void SetPath(PdfSubpath path)
+ public void SetPath(IReadOnlyList path)
{
- Path = path ?? throw new ArgumentNullException(nameof(path));
+ Path = path.ToList() ?? throw new ArgumentNullException(nameof(path));
}
public void ClearFlexPoints()
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStringParser.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStringParser.cs
index fca0219e..f1b5cde5 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStringParser.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStringParser.cs
@@ -1,35 +1,39 @@
namespace UglyToad.PdfPig.Fonts.Type1.CharStrings
{
- using System;
- using System.Collections.Generic;
using Commands;
using Commands.Arithmetic;
using Commands.Hint;
using Commands.PathConstruction;
using Commands.StartFinishOutline;
using Core;
- using Parser;
+ using System;
+ using System.Collections.Generic;
///
/// Decodes a set of CharStrings to their corresponding Type 1 BuildChar operations.
///
///
- /// A charstring is an encrypted sequence of unsigned 8-bit bytes that encode integers and commands.
+ ///
+ /// A charstring is an encrypted sequence of unsigned 8-bit bytes that encode integers and commands.
/// Type 1 BuildChar, when interpreting a charstring, will first decrypt it and then will decode
- /// its bytes one at a time in sequence.
- ///
+ /// its bytes one at a time in sequence.
+ ///
+ ///
/// The value in a byte indicates a command, a number, or subsequent bytes that are to be interpreted
/// in a special way.
- ///
+ ///
+ ///
/// Once the bytes are decoded into numbers and commands, the execution of these numbers and commands proceeds in a
- /// manner similar to the operation of the PostScript language. Type 1 BuildChar uses its own operand stack,
+ /// manner similar to the operation of the PostScript language. Type 1 BuildChar uses its own operand stack,
/// called the Type 1 BuildChar operand stack, that is distinct from the PostScript interpreter operand stack.
- ///
+ ///
+ ///
/// This stack holds up to 24 numeric entries. A number, decoded from a charstring, is pushed onto the Type 1
/// BuildChar operand stack. A command expects its arguments in order on this operand stack with all arguments generally taken
- /// from the bottom of the stack (first argument bottom-most);
+ /// from the bottom of the stack (first argument bottom-most);
/// however, some commands, particularly the subroutine commands, normally work from the top of the stack. If a command returns
/// results, they are pushed onto the Type 1 BuildChar operand stack (last result topmost).
+ ///
///
internal static class Type1CharStringParser
{
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStrings.cs b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStrings.cs
index 0f989d78..63e1692d 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStrings.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/CharStrings/Type1CharStrings.cs
@@ -10,7 +10,7 @@
{
private readonly IReadOnlyDictionary charStringIndexToName;
private readonly object locker = new object();
- private readonly Dictionary glyphs = new Dictionary();
+ private readonly Dictionary> glyphs = new Dictionary>();
public IReadOnlyDictionary CharStrings { get; }
@@ -24,9 +24,9 @@
Subroutines = subroutines ?? throw new ArgumentNullException(nameof(subroutines));
}
- public bool TryGenerate(string name, out PdfSubpath path)
+ public bool TryGenerate(string name, out IReadOnlyList path)
{
- path = default(PdfSubpath);
+ path = new List();
lock (locker)
{
if (glyphs.TryGetValue(name, out path))
@@ -54,7 +54,7 @@
return true;
}
- private PdfSubpath Run(CommandSequence sequence)
+ private IReadOnlyList Run(CommandSequence sequence)
{
var context = new Type1BuildCharContext(Subroutines, i =>
{
diff --git a/src/UglyToad.PdfPig.Fonts/Type1/Type1FontProgram.cs b/src/UglyToad.PdfPig.Fonts/Type1/Type1FontProgram.cs
index f3d9a1b1..ef97627c 100644
--- a/src/UglyToad.PdfPig.Fonts/Type1/Type1FontProgram.cs
+++ b/src/UglyToad.PdfPig.Fonts/Type1/Type1FontProgram.cs
@@ -1,9 +1,9 @@
namespace UglyToad.PdfPig.Fonts.Type1
{
- using System;
- using System.Collections.Generic;
using CharStrings;
using Core;
+ using System;
+ using System.Collections.Generic;
using Tokens;
///
@@ -62,14 +62,8 @@
///
public PdfRectangle? GetCharacterBoundingBox(string characterName)
{
- if (!CharStrings.TryGenerate(characterName, out var glyph))
- {
- return null;
- }
-
- var bbox = glyph.GetBoundingRectangle();
-
- return bbox;
+ var glyph = GetCharacterPath(characterName);
+ return PdfSubpath.GetBoundingRectangle(glyph);
}
///
@@ -96,5 +90,17 @@
return TransformationMatrix.FromValues(a, b, c, d, e, f);
}
+
+ ///
+ /// Get the pdfpath for the character with the given name.
+ ///
+ public IReadOnlyList GetCharacterPath(string characterName)
+ {
+ if (!CharStrings.TryGenerate(characterName, out var glyph))
+ {
+ return null;
+ }
+ return glyph;
+ }
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig.Tests/Fonts/TrueType/Parser/TrueTypeFontParserTests.cs b/src/UglyToad.PdfPig.Tests/Fonts/TrueType/Parser/TrueTypeFontParserTests.cs
index b20bd678..2136ac8c 100644
--- a/src/UglyToad.PdfPig.Tests/Fonts/TrueType/Parser/TrueTypeFontParserTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Fonts/TrueType/Parser/TrueTypeFontParserTests.cs
@@ -2,13 +2,18 @@
namespace UglyToad.PdfPig.Tests.Fonts.TrueType.Parser
{
using System;
+ using System.Collections.Generic;
+ using System.Drawing;
using System.Globalization;
+ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using PdfPig.Core;
using PdfPig.Fonts.TrueType;
using PdfPig.Fonts.TrueType.Parser;
using PdfPig.Fonts.TrueType.Tables;
+ using UglyToad.PdfPig.Fonts.TrueType.Glyphs;
+ using UglyToad.PdfPig.Graphics;
using Xunit;
public class TrueTypeFontParserTests
@@ -183,7 +188,7 @@ namespace UglyToad.PdfPig.Tests.Fonts.TrueType.Parser
var font = TrueTypeFontParser.Parse(input);
var robotoGlyphs = Encoding.ASCII.GetString(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.GlyphData.txt"));
- var lines = robotoGlyphs.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries);
+ var lines = robotoGlyphs.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
@@ -205,9 +210,16 @@ namespace UglyToad.PdfPig.Tests.Fonts.TrueType.Parser
{
Assert.Equal(width, glyph.Bounds.Width);
}
-
+
Assert.Equal(height, glyph.Bounds.Height);
Assert.Equal(points, glyph.Points.Length);
+ if (points > 0)
+ {
+ Assert.True(glyph.Points[glyph.Points.Length - 1].IsEndOfContour);
+ Assert.True(glyph.TryGetGlyphPath(out var subpaths));
+
+ // TODO - more tests on path
+ }
}
}
@@ -226,4 +238,3 @@ namespace UglyToad.PdfPig.Tests.Fonts.TrueType.Parser
}
}
}
-
diff --git a/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs b/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs
index 7a62be18..da1e0f1e 100644
--- a/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Fonts/Type1/Type1FontParserTests.cs
@@ -63,7 +63,7 @@
foreach (var charString in result.CharStrings.CharStrings)
{
Assert.True(result.CharStrings.TryGenerate(charString.Key, out var path));
- builder.AppendLine(path.ToFullSvg(0));
+ //builder.AppendLine(path.ToFullSvg(0)); // TODO - to restore
}
builder.Append("