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(""); diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfTextRemoverTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfTextRemoverTests.cs index 00814bfc..85239eff 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfTextRemoverTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfTextRemoverTests.cs @@ -1,6 +1,6 @@ -using UglyToad.PdfPig.Tests.Integration; +using System.IO; +using UglyToad.PdfPig.Tests.Integration; using UglyToad.PdfPig.Writer; -using System.IO; using Xunit; namespace UglyToad.PdfPig.Tests.Writer @@ -18,7 +18,8 @@ namespace UglyToad.PdfPig.Tests.Writer using (var document = PdfDocument.Open(filePath)) { var withoutText = PdfTextRemover.RemoveText(filePath); - File.WriteAllBytes(@"C:\temp\_tx.pdf", withoutText); + WriteFile($"{nameof(TextRemoverRemovesText)}_{file}", withoutText); + using (var documentWithoutText = PdfDocument.Open(withoutText)) { Assert.Equal(document.NumberOfPages, documentWithoutText.NumberOfPages); @@ -27,9 +28,27 @@ namespace UglyToad.PdfPig.Tests.Writer Assert.NotEqual(document.GetPage(i).Text, string.Empty); Assert.Equal(documentWithoutText.GetPage(i).Text, string.Empty); } - } } } + + private static void WriteFile(string name, byte[] bytes) + { + try + { + if (!Directory.Exists("Writer")) + { + Directory.CreateDirectory("Writer"); + } + + var output = Path.Combine("Writer", $"{name}"); + + File.WriteAllBytes(output, bytes); + } + catch + { + // ignored. + } + } } } diff --git a/src/UglyToad.PdfPig.sln b/src/UglyToad.PdfPig.sln index 909d1579..444459e4 100644 --- a/src/UglyToad.PdfPig.sln +++ b/src/UglyToad.PdfPig.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32819.101 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig", "UglyToad.PdfPig\UglyToad.PdfPig.csproj", "{57D0610C-87D3-4E0B-B7C2-EC8B765A8288}" EndProject @@ -18,9 +18,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Core", "Ugl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.DocumentLayoutAnalysis", "UglyToad.PdfPig.DocumentLayoutAnalysis\UglyToad.PdfPig.DocumentLayoutAnalysis.csproj", "{60126BCA-6C52-48A9-A0A6-51796C8B0BE7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UglyToad.PdfPig.Tokens", "UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj", "{D840FF69-4250-4B05-9829-5ABEC43EC82C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokens", "UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj", "{D840FF69-4250-4B05-9829-5ABEC43EC82C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UglyToad.PdfPig.Tokenization", "UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj", "{FD005C50-CD2C-497E-8F7E-6D791091E9B0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokenization", "UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj", "{FD005C50-CD2C-497E-8F7E-6D791091E9B0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/UglyToad.PdfPig/Graphics/PdfPath.cs b/src/UglyToad.PdfPig/Graphics/PdfPath.cs index f3a64430..3ce753fa 100644 --- a/src/UglyToad.PdfPig/Graphics/PdfPath.cs +++ b/src/UglyToad.PdfPig/Graphics/PdfPath.cs @@ -1,7 +1,6 @@ namespace UglyToad.PdfPig.Graphics { using System.Collections.Generic; - using System.Linq; using UglyToad.PdfPig.Core; using UglyToad.PdfPig.Graphics.Colors; using UglyToad.PdfPig.Graphics.Core; @@ -128,22 +127,7 @@ /// For paths which don't define any geometry this returns . public PdfRectangle? GetBoundingRectangle() { - if (this.Count == 0) - { - return null; - } - - var bboxes = this.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); + return PdfSubpath.GetBoundingRectangle(this); } } } diff --git a/src/UglyToad.PdfPig/Parser/PageFactory.cs b/src/UglyToad.PdfPig/Parser/PageFactory.cs index 94c53808..03467e70 100644 --- a/src/UglyToad.PdfPig/Parser/PageFactory.cs +++ b/src/UglyToad.PdfPig/Parser/PageFactory.cs @@ -87,7 +87,7 @@ pdfScanner, filterProvider, resourceStore); - // ignored for now, is it possible? check the spec... + // ignored for now, is it possible? check the spec... } else if (DirectObjectFinder.TryGet(contents, pdfScanner, out var array)) { diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs index d26ef5b9..415f3362 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs @@ -2,10 +2,12 @@ { using Core; using Geometry; + using System.Collections.Generic; + using System; using Tokens; /// - /// A CID font contains glyph descriptions accessed by + /// A CID font contains glyph descriptions accessed by /// CID (character identifier) as character selectors. /// /// @@ -51,5 +53,35 @@ PdfVector GetPositionVector(int characterIdentifier); PdfVector GetDisplacementVector(int characterIdentifier); + + /// + /// Returns the glyph path for the given character code. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// The glyph path for the given character code. + bool TryGetPath(int characterCode, out IReadOnlyList path); + + /// + /// Returns the glyph path for the given character code. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// + /// The glyph path for the given character code. + bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path); + + /// + /// Returns the normalised glyph path for the given character code in a PDF. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// The normalized glyph path for the given character code. + bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path); + + /// + /// Returns the normalised glyph path for the given character code in a PDF. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// + /// The normalized glyph path for the given character code. + bool TryGetNormalisedPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path); } } \ No newline at end of file diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs index fd9414ae..4a8e8e4c 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs @@ -1,6 +1,7 @@ namespace UglyToad.PdfPig.PdfFonts.CidFonts { using System; + using System.Collections.Generic; using Core; /// @@ -18,6 +19,10 @@ bool TryGetBoundingAdvancedWidth(int characterIdentifier, out double width); + bool TryGetPath(int characterName, out IReadOnlyList path); + + bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path); + int GetFontMatrixMultiplier(); } } diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs index 05d1beef..23f69da1 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs @@ -1,6 +1,7 @@ namespace UglyToad.PdfPig.PdfFonts.CidFonts { using System; + using System.Collections.Generic; using Core; using Fonts.CompactFontFormat; @@ -62,7 +63,6 @@ return true; } - public bool TryGetBoundingBox(int characterIdentifier, Func characterCodeToGlyphId, out PdfRectangle boundingBox) { throw new NotImplementedException(); @@ -106,5 +106,31 @@ #endif return fontCollection.FirstFont; } + + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + path = EmptyArray.Instance; + + var font = GetFont(); + + if (font.Encoding == null) + { + return false; + } + + var characterName = GetCharacterName(characterCode); + + if (font.TryGetPath(characterName, out path)) + { + return true; + } + + return false; + } + + public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + { + throw new NotImplementedException(); + } } } diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs index cfe66319..7ad9a0ba 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs @@ -1,6 +1,7 @@ namespace UglyToad.PdfPig.PdfFonts.CidFonts { using System; + using System.Collections.Generic; using Core; using Fonts.TrueType; using Fonts.TrueType.Tables; @@ -10,7 +11,7 @@ private readonly TrueTypeFont font; public FontDetails Details { get; } - + public PdfCidTrueTypeFont(TrueTypeFont font) { this.font = font ?? throw new ArgumentNullException(nameof(font)); @@ -33,5 +34,10 @@ => font.TryGetAdvanceWidth(characterIdentifier, characterCodeToGlyphId, out width); public int GetFontMatrixMultiplier() => font.GetUnitsPerEm(); + + public bool TryGetPath(int characterCode, out IReadOnlyList path) => font.TryGetPath(characterCode, out path); + + public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + => font.TryGetPath(characterCode, characterCodeToGlyphId, out path); } } \ No newline at end of file diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs index 275b441c..4b0d76f6 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs @@ -131,5 +131,37 @@ { return verticalWritingMetrics.GetDisplacementVector(characterIdentifier); } + + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + path = null; + if (fontProgram == null) + { + return false; + } + + return fontProgram.TryGetPath(characterCode, out path); + } + + public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + { + path = null; + if (fontProgram == null) + { + return false; + } + + return fontProgram.TryGetPath(characterCode, characterCodeToGlyphId, out path); + } + + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + return TryGetPath(characterCode, out path); + } + + public bool TryGetNormalisedPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + { + return TryGetPath(characterCode, characterCodeToGlyphId, out path); + } } } diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs index 689c53ac..ad015f6b 100644 --- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs @@ -1,6 +1,8 @@ namespace UglyToad.PdfPig.PdfFonts.CidFonts { + using System; using System.Collections.Generic; + using System.Linq; using Core; using Geometry; using Tokens; @@ -114,5 +116,34 @@ { return verticalWritingMetrics.GetDisplacementVector(characterIdentifier); } + + public bool TryGetPath(int characterCode, out IReadOnlyList path) => TryGetPath(characterCode, cidToGid.GetGlyphIndex, out path); + + public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + { + path = null; + if (fontProgram == null) + { + return false; + } + + return fontProgram.TryGetPath(characterCode, characterCodeToGlyphId, out path); + } + + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + if (!TryGetPath(characterCode, out path)) + { + return false; + } + + path = FontMatrix.Transform(path).ToList(); + return true; + } + + public bool TryGetNormalisedPath(int characterCode, Func characterCodeToGlyphId, out IReadOnlyList path) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs b/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs index c41f31bc..2d120aa8 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs @@ -1,165 +1,177 @@ -namespace UglyToad.PdfPig.PdfFonts.Composite -{ - using System; - using System.Collections.Generic; - using CidFonts; - using Cmap; - using Core; - using Geometry; - using Tokens; - using Util.JetBrains.Annotations; - using Debug = System.Diagnostics.Debug; - /// - /// Defines glyphs using a CIDFont - /// - internal class Type0Font : IFont, IVerticalWritingSupported - { - private readonly CMap ucs2CMap; - // ReSharper disable once NotAccessedField.Local - private readonly bool isChineseJapaneseOrKorean; - private readonly Dictionary boundingBoxCache - = new Dictionary(); - - public NameToken Name => BaseFont; - - [NotNull] - public NameToken BaseFont { get; } - - [NotNull] - public ICidFont CidFont { get; } - - [NotNull] - public CMap CMap { get; } - - [NotNull] - public ToUnicodeCMap ToUnicode { get; } - - public bool IsVertical => CMap.WritingMode == WritingMode.Vertical; - - public FontDetails Details { get; } - - public Type0Font(NameToken baseFont, ICidFont cidFont, CMap cmap, CMap toUnicodeCMap, - CMap ucs2CMap, - bool isChineseJapaneseOrKorean) - { - this.ucs2CMap = ucs2CMap; - this.isChineseJapaneseOrKorean = isChineseJapaneseOrKorean; - - BaseFont = baseFont ?? throw new ArgumentNullException(nameof(baseFont)); - CidFont = cidFont ?? throw new ArgumentNullException(nameof(cidFont)); - CMap = cmap ?? throw new ArgumentNullException(nameof(cmap)); - ToUnicode = new ToUnicodeCMap(toUnicodeCMap); - Details = cidFont.Details?.WithName(Name.Data) - ?? FontDetails.GetDefault(Name.Data); - } - - public int ReadCharacterCode(IInputBytes bytes, out int codeLength) - { - var current = bytes.CurrentOffset; - - var code = CMap.ReadCode(bytes); - - codeLength = (int)(bytes.CurrentOffset - current); - - return code; - } - - public bool TryGetUnicode(int characterCode, out string value) - { +namespace UglyToad.PdfPig.PdfFonts.Composite +{ + using CidFonts; + using Cmap; + using Core; + using Geometry; + using System; + using System.Collections.Generic; + using Tokens; + using Util.JetBrains.Annotations; + using Debug = System.Diagnostics.Debug; + + /// + /// Defines glyphs using a CIDFont + /// + internal class Type0Font : IFont, IVerticalWritingSupported + { + private readonly CMap ucs2CMap; + // ReSharper disable once NotAccessedField.Local + private readonly bool isChineseJapaneseOrKorean; + private readonly Dictionary boundingBoxCache + = new Dictionary(); + + public NameToken Name => BaseFont; + + [NotNull] + public NameToken BaseFont { get; } + + [NotNull] + public ICidFont CidFont { get; } + + [NotNull] + public CMap CMap { get; } + + [NotNull] + public ToUnicodeCMap ToUnicode { get; } + + public bool IsVertical => CMap.WritingMode == WritingMode.Vertical; + + public FontDetails Details { get; } + + public Type0Font(NameToken baseFont, ICidFont cidFont, CMap cmap, CMap toUnicodeCMap, + CMap ucs2CMap, + bool isChineseJapaneseOrKorean) + { + this.ucs2CMap = ucs2CMap; + this.isChineseJapaneseOrKorean = isChineseJapaneseOrKorean; + + BaseFont = baseFont ?? throw new ArgumentNullException(nameof(baseFont)); + CidFont = cidFont ?? throw new ArgumentNullException(nameof(cidFont)); + CMap = cmap ?? throw new ArgumentNullException(nameof(cmap)); + ToUnicode = new ToUnicodeCMap(toUnicodeCMap); + Details = cidFont.Details?.WithName(Name.Data) + ?? FontDetails.GetDefault(Name.Data); + } + + public int ReadCharacterCode(IInputBytes bytes, out int codeLength) + { + var current = bytes.CurrentOffset; + + var code = CMap.ReadCode(bytes); + + codeLength = (int)(bytes.CurrentOffset - current); + + return code; + } + + public bool TryGetUnicode(int characterCode, out string value) + { value = null; - var HaveCMap = ToUnicode.CanMapToUnicode; - if (HaveCMap == false) - { - var HaveUnicode2CMap = (ucs2CMap is null == false); + var HaveCMap = ToUnicode.CanMapToUnicode; + if (HaveCMap == false) + { + var HaveUnicode2CMap = (ucs2CMap is null == false); if (HaveUnicode2CMap) { // Have both ucs2Map and CMap convert to unicode by // characterCode ----by CMAP---> CID ---ucs2Map---> Unicode - var CID = CMap.ConvertToCid(characterCode); - if (CID == 0) - { - Debug.WriteLine($"Warning: No mapping from characterCode (0x{characterCode:X} to CID by ucs2Map."); - return false; // No mapping from characterCode to CID. - } - // CID ---ucs2Map---> Unicode - if (ucs2CMap.TryConvertToUnicode(CID, out value)) - { - return value != null; - } - - } - if (HaveUnicode2CMap) // 2022-12-24 @fnatzke left as fall-back. Possible? - { - // characterCode ---ucs2Map---> Unicode (?) @fnatzke possible? - if (ucs2CMap.TryConvertToUnicode(characterCode, out value)) - { - return value != null; - } - } - } - - // According to PdfBox certain providers incorrectly using Identity CMaps as ToUnicode. - if (ToUnicode.IsUsingIdentityAsUnicodeMap) - { - value = new string((char)characterCode, 1); - - return true; - } - - return ToUnicode.TryGet(characterCode, out value); - } - - public CharacterBoundingBox GetBoundingBox(int characterCode) - { - if (boundingBoxCache.TryGetValue(characterCode, out var cached)) - { - return cached; - } - - var matrix = GetFontMatrix(); - - var boundingBox = GetBoundingBoxInGlyphSpace(characterCode); - - boundingBox = matrix.Transform(boundingBox); - - var characterIdentifier = CMap.ConvertToCid(characterCode); - - var width = CidFont.GetWidthFromFont(characterIdentifier); - - var advanceWidth = matrix.TransformX(width); - - var result = new CharacterBoundingBox(boundingBox, advanceWidth); - - boundingBoxCache[characterCode] = result; - - return result; - } - - public PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode) - { - var characterIdentifier = CMap.ConvertToCid(characterCode); - - return CidFont.GetBoundingBox(characterIdentifier); - } - - public TransformationMatrix GetFontMatrix() - { - return CidFont.FontMatrix; - } - - public PdfVector GetPositionVector(int characterCode) - { - var characterIdentifier = CMap.ConvertToCid(characterCode); - - return CidFont.GetPositionVector(characterIdentifier).Scale(-1 / 1000.0); - } - - public PdfVector GetDisplacementVector(int characterCode) - { - var characterIdentifier = CMap.ConvertToCid(characterCode); - - return CidFont.GetDisplacementVector(characterIdentifier).Scale(1 / 1000.0); - } - } -} + var CID = CMap.ConvertToCid(characterCode); + if (CID == 0) + { + Debug.WriteLine($"Warning: No mapping from characterCode (0x{characterCode:X} to CID by ucs2Map."); + return false; // No mapping from characterCode to CID. + } + // CID ---ucs2Map---> Unicode + if (ucs2CMap.TryConvertToUnicode(CID, out value)) + { + return value != null; + } + } + if (HaveUnicode2CMap) // 2022-12-24 @fnatzke left as fall-back. Possible? + { + // characterCode ---ucs2Map---> Unicode (?) @fnatzke possible? + if (ucs2CMap.TryConvertToUnicode(characterCode, out value)) + { + return value != null; + } + } + } + + // According to PdfBox certain providers incorrectly using Identity CMaps as ToUnicode. + if (ToUnicode.IsUsingIdentityAsUnicodeMap) + { + value = new string((char)characterCode, 1); + + return true; + } + + return ToUnicode.TryGet(characterCode, out value); + } + + public CharacterBoundingBox GetBoundingBox(int characterCode) + { + if (boundingBoxCache.TryGetValue(characterCode, out var cached)) + { + return cached; + } + + var matrix = GetFontMatrix(); + + var boundingBox = GetBoundingBoxInGlyphSpace(characterCode); + + boundingBox = matrix.Transform(boundingBox); + + var characterIdentifier = CMap.ConvertToCid(characterCode); + + var width = CidFont.GetWidthFromFont(characterIdentifier); + + var advanceWidth = matrix.TransformX(width); + + var result = new CharacterBoundingBox(boundingBox, advanceWidth); + + boundingBoxCache[characterCode] = result; + + return result; + } + + public PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode) + { + var characterIdentifier = CMap.ConvertToCid(characterCode); + + return CidFont.GetBoundingBox(characterIdentifier); + } + + public TransformationMatrix GetFontMatrix() + { + return CidFont.FontMatrix; + } + + public PdfVector GetPositionVector(int characterCode) + { + var characterIdentifier = CMap.ConvertToCid(characterCode); + + return CidFont.GetPositionVector(characterIdentifier).Scale(-1 / 1000.0); + } + + public PdfVector GetDisplacementVector(int characterCode) + { + var characterIdentifier = CMap.ConvertToCid(characterCode); + + return CidFont.GetDisplacementVector(characterIdentifier).Scale(1 / 1000.0); + } + + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + return CidFont.TryGetPath(characterCode, out path); + } + + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + return CidFont.TryGetNormalisedPath(characterCode, out path); + } + } +} diff --git a/src/UglyToad.PdfPig/PdfFonts/IFont.cs b/src/UglyToad.PdfPig/PdfFonts/IFont.cs index 26b6a978..9bde66d1 100644 --- a/src/UglyToad.PdfPig/PdfFonts/IFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/IFont.cs @@ -1,12 +1,13 @@ namespace UglyToad.PdfPig.PdfFonts { using Core; + using System.Collections.Generic; using Tokens; internal interface IFont { NameToken Name { get; } - + bool IsVertical { get; } FontDetails Details { get; } @@ -18,5 +19,19 @@ CharacterBoundingBox GetBoundingBox(int characterCode); TransformationMatrix GetFontMatrix(); + + /// + /// Returns the glyph path for the given character code. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// The glyph path for the given character code. + bool TryGetPath(int characterCode, out IReadOnlyList path); + + /// + /// Returns the normalised glyph path for the given character code in a PDF. + /// + /// Character code in a PDF. Not to be confused with unicode. + /// The normalized glyph path for the given character code. + bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path); } } diff --git a/src/UglyToad.PdfPig/PdfFonts/Parser/Parts/CidFontFactory.cs b/src/UglyToad.PdfPig/PdfFonts/Parser/Parts/CidFontFactory.cs index 8cbdb01d..4ebef127 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Parser/Parts/CidFontFactory.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Parser/Parts/CidFontFactory.cs @@ -114,11 +114,11 @@ switch (descriptor.FontFile.FileType) { case DescriptorFontFile.FontFileType.TrueType: - { - var input = new TrueTypeDataBytes(new ByteArrayInputBytes(fontFile)); - var ttf = TrueTypeFontParser.Parse(input); - return new PdfCidTrueTypeFont(ttf); - } + { + var input = new TrueTypeDataBytes(new ByteArrayInputBytes(fontFile)); + var ttf = TrueTypeFontParser.Parse(input); + return new PdfCidTrueTypeFont(ttf); + } case DescriptorFontFile.FontFileType.FromSubtype: { if (!DirectObjectFinder.TryGet(descriptor.FontFile.ObjectKey, pdfScanner, out StreamToken str)) @@ -145,7 +145,7 @@ var ttf = TrueTypeFontParser.Parse(new TrueTypeDataBytes(new ByteArrayInputBytes(bytes))); return new PdfCidTrueTypeFont(ttf); } - + throw new PdfDocumentFormatException($"Unexpected subtype for CID font: {subtypeName}."); } default: diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs index f1a3c959..6b9b09ac 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs @@ -1,13 +1,14 @@ namespace UglyToad.PdfPig.PdfFonts.Simple { - using System; - using System.Collections.Generic; using Cmap; using Composite; using Core; using Fonts; using Fonts.Encodings; using Fonts.TrueType; + using System; + using System.Collections.Generic; + using System.Linq; using Tokens; using Util.JetBrains.Annotations; @@ -57,7 +58,7 @@ Name = name; IsVertical = false; ToUnicode = new ToUnicodeCMap(toUnicodeCMap); - Details = descriptor?.ToDetails(Name?.Data) + Details = descriptor?.ToDetails(Name?.Data) ?? FontDetails.GetDefault(Name?.Data); } @@ -321,6 +322,29 @@ return widths[index]; } + + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + if (font == null) + { + path = EmptyArray.Instance; + return false; + } + + return font.TryGetPath(characterCode, CharacterCodeToGlyphId, out path); + } + + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + if (!TryGetPath(characterCode, out path)) + { + return false; + } + + path = GetFontMatrix().Transform(path).ToList(); + return true; + } } } - diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs index d3197c2b..97df1a9f 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Linq; using Core; using Fonts; using Fonts.AdobeFontMetrics; @@ -132,6 +133,29 @@ return DefaultTransformation; } + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + path = null; + if (font == null) + { + return false; + } + return font.TryGetPath(characterCode, out path); + } + + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + if (!TryGetPath(characterCode, out path)) + { + return false; + } + + path = GetFontMatrix().Transform(path).ToList(); + return true; + } + public class MetricOverrides { public int? FirstCharacterCode { get; } diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs index 72e80a2b..73e2d07e 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs @@ -1,6 +1,7 @@ namespace UglyToad.PdfPig.PdfFonts.Simple { using System.Collections.Generic; + using System.Linq; using Cmap; using Composite; using Core; @@ -43,7 +44,7 @@ public FontDetails Details { get; } - public Type1FontSimple(NameToken name, int firstChar, int lastChar, double[] widths, FontDescriptor fontDescriptor, Encoding encoding, + public Type1FontSimple(NameToken name, int firstChar, int lastChar, double[] widths, FontDescriptor fontDescriptor, Encoding encoding, CMap toUnicodeCMap, Union fontProgram) { @@ -72,7 +73,7 @@ fontMatrix = matrix; Name = name; - Details = fontDescriptor?.ToDetails(name?.Data) + Details = fontDescriptor?.ToDetails(name?.Data) ?? FontDetails.GetDefault(name?.Data); } @@ -116,7 +117,7 @@ } var name = encoding.GetName(characterCode); - + try { value = GlyphList.AdobeGlyphList.NameToUnicode(name); @@ -135,7 +136,7 @@ { return box; } - + var boundingBox = GetBoundingBoxInGlyphSpace(characterCode); var matrix = fontMatrix; @@ -144,7 +145,7 @@ var width = GetWidth(characterCode, boundingBox); - var result = new CharacterBoundingBox(boundingBox, width/1000.0); + var result = new CharacterBoundingBox(boundingBox, width / 1000.0); cachedBoundingBoxes[characterCode] = result; @@ -167,7 +168,7 @@ return boundingBox.Width; } - + private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode) { if (characterCode < firstChar || characterCode > lastChar) @@ -183,26 +184,25 @@ PdfRectangle? rect = null; if (fontProgram.TryGetFirst(out var t1Font)) { - var name = encoding.GetName(characterCode); - rect = t1Font.GetCharacterBoundingBox(name); + var name = encoding.GetName(characterCode); + rect = t1Font.GetCharacterBoundingBox(name); } else if (fontProgram.TryGetSecond(out var cffFont)) { - var first = cffFont.FirstFont; - string characterName; - if (encoding != null) - { - characterName = encoding.GetName(characterCode); - } - else - { - characterName = cffFont.GetCharacterName(characterCode); - } + var first = cffFont.FirstFont; + string characterName; + if (encoding != null) + { + characterName = encoding.GetName(characterCode); + } + else + { + characterName = cffFont.GetCharacterName(characterCode); + } - rect = first.GetCharacterBoundingBox(characterName); + rect = first.GetCharacterBoundingBox(characterName); } - if (!rect.HasValue) { return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0); @@ -216,5 +216,61 @@ { return fontMatrix; } + + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + path = null; + IReadOnlyList tempPath = null; + if (characterCode < firstChar || characterCode > lastChar) + { + return false; + } + + if (fontProgram == null) + { + return false; + } + + if (fontProgram.TryGetFirst(out var t1Font)) + { + var name = encoding.GetName(characterCode); + tempPath = t1Font.GetCharacterPath(name); + } + else if (fontProgram.TryGetSecond(out var cffFont)) + { + var first = cffFont.FirstFont; + string characterName; + if (encoding != null) + { + characterName = encoding.GetName(characterCode); + } + else + { + characterName = cffFont.GetCharacterName(characterCode); + } + + tempPath = first.GetCharacterPath(characterName); + } + + if (tempPath != null) + { + path = tempPath; + return true; + } + + return false; + } + + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + if (TryGetPath(characterCode, out path)) + { + path = fontMatrix.Transform(path).ToList(); + return true; + } + return false; + } } } diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs index 70160a2a..624abfbe 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs @@ -1,12 +1,13 @@ -// ReSharper disable CompareOfFloatsByEqualityOperator +//// ReSharper disable CompareOfFloatsByEqualityOperator namespace UglyToad.PdfPig.PdfFonts.Simple { - using System; - using System.Diagnostics; using Core; using Fonts; using Fonts.AdobeFontMetrics; using Fonts.Encodings; + using System; + using System.Collections.Generic; + using System.Diagnostics; using Tokens; /// @@ -120,5 +121,25 @@ namespace UglyToad.PdfPig.PdfFonts.Simple { return fontMatrix; } + + /// + /// + /// Not implemeted. + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java + path = null; + return false; + } + + /// + /// + /// Not implemeted. + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + return TryGetPath(characterCode, out path); + } } } diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs index 6c6db18a..607e9a2c 100644 --- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs +++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs @@ -5,6 +5,7 @@ using Core; using Fonts; using Fonts.Encodings; + using System.Collections.Generic; using Tokens; internal class Type3Font : IFont @@ -57,9 +58,7 @@ var name = encoding.GetName(characterCode); - var listed = GlyphList.AdobeGlyphList.NameToUnicode(name); - - value = listed; + value = GlyphList.AdobeGlyphList.NameToUnicode(name); return true; } @@ -89,5 +88,24 @@ { return fontMatrix; } + + /// + /// + /// Type 3 fonts do not use vector paths. Always returns false. + /// + public bool TryGetPath(int characterCode, out IReadOnlyList path) + { + path = null; + return false; + } + + /// + /// + /// Type 3 fonts do not use vector paths. Always returns false. + /// + public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList path) + { + return TryGetPath(characterCode, out path); + } } }