Implement TryGetPath and TryGetNormalisedPath for fonts

This commit is contained in:
BobLD 2023-01-31 17:07:07 +00:00
parent 5eebe9d0f9
commit cc6e2d302f
47 changed files with 1884 additions and 1256 deletions

View File

@ -196,7 +196,7 @@
throw new ArgumentNullException("BezierCurveTo(): currentPosition is null.");
}
}
/// <summary>
/// Close the path.
/// </summary>
@ -212,7 +212,7 @@
}
commands.Add(new Close());
}
/// <summary>
/// Determines if the path is currently closed.
/// </summary>
@ -350,6 +350,30 @@
return new PdfRectangle(mv.Location, new PdfPoint(mv.Location.X + width, mv.Location.Y + height));
}
/// <summary>
/// Gets a <see cref="PdfRectangle"/> which entirely contains the geometry of the defined path.
/// </summary>
/// <returns>For paths which don't define any geometry this returns <see langword="null"/>.</returns>
public static PdfRectangle? GetBoundingRectangle(IReadOnlyList<PdfSubpath> 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);
}
/// <summary>
/// A command in a <see cref="PdfSubpath"/>.
/// </summary>

View File

@ -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;
/// <summary>
/// Specifies the conversion from the transformed coordinate space to the original untransformed coordinate space.
@ -256,6 +259,59 @@
);
}
/// <summary>
/// Transform a subpath using this transformation matrix.
/// </summary>
/// <param name="subpath">The original subpath.</param>
/// <returns>A new subpath which is the result of applying this transformation matrix.</returns>
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;
}
/// <summary>
/// Transform a path using this transformation matrix.
/// </summary>
/// <param name="path">The original path.</param>
/// <returns>A new path which is the result of applying this transformation matrix.</returns>
public IEnumerable<PdfSubpath> Transform(IEnumerable<PdfSubpath> path)
{
foreach (var subpath in path)
{
yield return Transform(subpath);
}
}
/// <summary>
/// Generate a <see cref="TransformationMatrix"/> translated by the specified amount.
/// </summary>

View File

@ -1,7 +1,7 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
using System.Collections.Generic;
using Core;
using System.Collections.Generic;
/// <summary>
/// The context used and updated when interpreting the commands for a charstring.
@ -18,7 +18,7 @@
/// <summary>
/// The current path.
/// </summary>
public PdfSubpath Path { get; } = new PdfSubpath();
public List<PdfSubpath> Path { get; } = new List<PdfSubpath>();
/// <summary>
/// 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;
}
/// <summary>
/// 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.
/// <para>See 4.1 Path Construction Operators in 'The Type 2 Charstring Format, Technical Note #5177', 16 March 2000</para>
/// <see href="https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf"/>
/// </summary>
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];

View File

@ -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;
/// <summary>
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
@ -21,7 +21,6 @@
/// </summary>
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> 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 @@
/// <summary>
/// The path of the glyph.
/// </summary>
public PdfSubpath Path { get; }
public IReadOnlyList<PdfSubpath> Path { get; }
/// <summary>
/// The width of the glyph as a difference from the nominal width X for the font. Optional.
@ -234,7 +233,7 @@
/// <summary>
/// Create a new <see cref="Type2Glyph"/>.
/// </summary>
public Type2Glyph(PdfSubpath path, double? width)
public Type2Glyph(IReadOnlyList<PdfSubpath> path, double? width)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Width = width;

View File

@ -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;
/// <summary>
@ -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);
}
/// <summary>
/// Get the pdfpath for the character with the given name.
/// </summary>
/// <param name="characterName"></param>
/// <param name="path"></param>
/// <returns></returns>
public bool TryGetPath(string characterName, out IReadOnlyList<PdfSubpath> 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;
}
/// <summary>
/// GetCharacterPath
/// </summary>
/// <param name="characterName"></param>
/// <returns></returns>
public IReadOnlyList<PdfSubpath> 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;
}
/// <summary>

View File

@ -15,7 +15,7 @@
private readonly IReadOnlyDictionary<string, string> nameToUnicode;
private readonly IReadOnlyDictionary<string, string> unicodeToName;
private readonly Dictionary<string, string> oddNameToUnicodeCache = new Dictionary<string, string>();
private static readonly Lazy<GlyphList> LazyAdobeGlyphList = new Lazy<GlyphList>(() => 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 @@
}
}
}

View File

@ -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<PdfSubpath> subpaths)
{
subpaths = EmptyArray<PdfSubpath>.Instance;
if (Points == null)
{
return false;
}
if (Points.Length > 0)
{
subpaths = CalculatePath(Points);
}
return true;
}
private static IReadOnlyList<PdfSubpath> CalculatePath(GlyphPoint[] points)
{
// https://github.com/apache/pdfbox/blob/trunk/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphRenderer.java
var path = new List<PdfSubpath>();
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<GlyphPoint>();
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);
}
/// <summary>
/// This creates an onCurve point that is between point1 and point2.
/// </summary>
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";

View File

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

View File

@ -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<PdfSubpath> subpaths);
IGlyphDescription DeepClone();
}
}

View File

@ -9,7 +9,7 @@
public class TableRegister
{
/// <summary>
/// This table contains global information about the font.
/// This table contains global information about the font.
/// </summary>
public HeaderTable HeaderTable { get; }

View File

@ -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<IReadOnlyList<IGlyphDescription>> glyphs;
public IReadOnlyList<IGlyphDescription> Glyphs => glyphs.Value;
public GlyphDataTable(TrueTypeHeaderTable directoryTable, IReadOnlyList<uint> glyphOffsets,
PdfRectangle maxGlyphBounds,
public GlyphDataTable(TrueTypeHeaderTable directoryTable, IReadOnlyList<uint> 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<byte>.Instance, EmptyArray<ushort>.Instance,
EmptyArray<GlyphPoint>.Instance,
EmptyArray<GlyphPoint>.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<CompositeComponent>();
// 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.
/// </summary>
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));
}

View File

@ -112,7 +112,7 @@
{
return false;
}
if (!TableRegister.GlyphTable.TryGetGlyphBounds(index, out boundingBox))
{
return false;
@ -126,6 +126,28 @@
return true;
}
/// <summary>
/// Try to get the bounding box for a glyph representing the specified character code if present.
/// </summary>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path) => TryGetPath(characterCode, null, out path);
/// <summary>
/// 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.
/// </summary>
public bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
path = EmptyArray<PdfSubpath>.Instance;
if (!TryGetGlyphIndex(characterCode, characterCodeToGlyphId, out var index)
|| TableRegister.GlyphTable == null)
{
return false;
}
return TableRegister.GlyphTable.Glyphs[index].TryGetGlyphPath(out path);
}
/// <summary>
/// Try to get the advance width for a glyph representing the specified character code if present.
/// </summary>

View File

@ -17,7 +17,7 @@
public static void Run(Type1BuildCharContext context)
{
context.Path.CloseSubpath();
context.Path[context.Path.Count - 1].CloseSubpath();
context.Stack.Clear();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,12 +2,13 @@
{
using System;
using System.Collections.Generic;
using System.Linq;
using Core;
internal class Type1BuildCharContext
{
private readonly Func<int, PdfSubpath> characterByIndexFactory;
private readonly Func<string, PdfSubpath> characterByNameFactory;
private readonly Func<int, IReadOnlyList<PdfSubpath>> characterByIndexFactory;
private readonly Func<string, IReadOnlyList<PdfSubpath>> characterByNameFactory;
public IReadOnlyDictionary<int, Type1CharStrings.CommandSequence> 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<PdfSubpath> Path { get; private set; } = new List<PdfSubpath>();
public PdfPoint CurrentPosition { get; set; }
@ -31,8 +32,8 @@
public List<PdfPoint> FlexPoints { get; } = new List<PdfPoint>();
public Type1BuildCharContext(IReadOnlyDictionary<int, Type1CharStrings.CommandSequence> subroutines,
Func<int, PdfSubpath> characterByIndexFactory,
Func<string, PdfSubpath> characterByNameFactory)
Func<int, IReadOnlyList<PdfSubpath>> characterByIndexFactory,
Func<string, IReadOnlyList<PdfSubpath>> 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<PdfSubpath> GetCharacter(int characterCode)
{
return characterByIndexFactory(characterCode);
}
public PdfSubpath GetCharacter(string characterName)
public IReadOnlyList<PdfSubpath> GetCharacter(string characterName)
{
return characterByNameFactory(characterName);
}
public void SetPath(PdfSubpath path)
public void SetPath(IReadOnlyList<PdfSubpath> path)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Path = path.ToList() ?? throw new ArgumentNullException(nameof(path));
}
public void ClearFlexPoints()

View File

@ -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;
/// <summary>
/// Decodes a set of CharStrings to their corresponding Type 1 BuildChar operations.
/// </summary>
/// <remarks>
/// A charstring is an encrypted sequence of unsigned 8-bit bytes that encode integers and commands.
/// <para>
/// 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.
/// </para>
/// <para>
/// The value in a byte indicates a command, a number, or subsequent bytes that are to be interpreted
/// in a special way.
///
/// </para>
/// <para>
/// 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.
///
/// </para>
/// <para>
/// 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).
/// </para>
/// </remarks>
internal static class Type1CharStringParser
{

View File

@ -10,7 +10,7 @@
{
private readonly IReadOnlyDictionary<int, string> charStringIndexToName;
private readonly object locker = new object();
private readonly Dictionary<string, PdfSubpath> glyphs = new Dictionary<string, PdfSubpath>();
private readonly Dictionary<string, IReadOnlyList<PdfSubpath>> glyphs = new Dictionary<string, IReadOnlyList<PdfSubpath>>();
public IReadOnlyDictionary<string, CommandSequence> 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<PdfSubpath> path)
{
path = default(PdfSubpath);
path = new List<PdfSubpath>();
lock (locker)
{
if (glyphs.TryGetValue(name, out path))
@ -54,7 +54,7 @@
return true;
}
private PdfSubpath Run(CommandSequence sequence)
private IReadOnlyList<PdfSubpath> Run(CommandSequence sequence)
{
var context = new Type1BuildCharContext(Subroutines, i =>
{

View File

@ -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;
/// <summary>
@ -62,14 +62,8 @@
/// </summary>
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);
}
/// <summary>
@ -96,5 +90,17 @@
return TransformationMatrix.FromValues(a, b, c, d, e, f);
}
/// <summary>
/// Get the pdfpath for the character with the given name.
/// </summary>
public IReadOnlyList<PdfSubpath> GetCharacterPath(string characterName)
{
if (!CharStrings.TryGenerate(characterName, out var glyph))
{
return null;
}
return glyph;
}
}
}

View File

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

View File

@ -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("</body></html>");

View File

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

View File

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

View File

@ -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 @@
/// <returns>For paths which don't define any geometry this returns <see langword="null"/>.</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);
}
}
}

View File

@ -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<ArrayToken>(contents, pdfScanner, out var array))
{

View File

@ -2,10 +2,12 @@
{
using Core;
using Geometry;
using System.Collections.Generic;
using System;
using Tokens;
/// <summary>
/// A CID font contains glyph descriptions accessed by
/// A CID font contains glyph descriptions accessed by
/// CID (character identifier) as character selectors.
/// </summary>
/// <remarks>
@ -51,5 +53,35 @@
PdfVector GetPositionVector(int characterIdentifier);
PdfVector GetDisplacementVector(int characterIdentifier);
/// <summary>
/// Returns the glyph path for the given character code.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="path">The glyph path for the given character code.</param>
bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path);
/// <summary>
/// Returns the glyph path for the given character code.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="characterCodeToGlyphId"></param>
/// <param name="path">The glyph path for the given character code.</param>
bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path);
/// <summary>
/// Returns the normalised glyph path for the given character code in a PDF.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="path">The normalized glyph path for the given character code.</param>
bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path);
/// <summary>
/// Returns the normalised glyph path for the given character code in a PDF.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="characterCodeToGlyphId"></param>
/// <param name="path">The normalized glyph path for the given character code.</param>
bool TryGetNormalisedPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path);
}
}

View File

@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.PdfFonts.CidFonts
{
using System;
using System.Collections.Generic;
using Core;
/// <summary>
@ -18,6 +19,10 @@
bool TryGetBoundingAdvancedWidth(int characterIdentifier, out double width);
bool TryGetPath(int characterName, out IReadOnlyList<PdfSubpath> path);
bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path);
int GetFontMatrixMultiplier();
}
}

View File

@ -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<int, int?> characterCodeToGlyphId, out PdfRectangle boundingBox)
{
throw new NotImplementedException();
@ -106,5 +106,31 @@
#endif
return fontCollection.FirstFont;
}
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
path = EmptyArray<PdfSubpath>.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<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
throw new NotImplementedException();
}
}
}

View File

@ -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<PdfSubpath> path) => font.TryGetPath(characterCode, out path);
public bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
=> font.TryGetPath(characterCode, characterCodeToGlyphId, out path);
}
}

View File

@ -131,5 +131,37 @@
{
return verticalWritingMetrics.GetDisplacementVector(characterIdentifier);
}
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
path = null;
if (fontProgram == null)
{
return false;
}
return fontProgram.TryGetPath(characterCode, out path);
}
public bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
path = null;
if (fontProgram == null)
{
return false;
}
return fontProgram.TryGetPath(characterCode, characterCodeToGlyphId, out path);
}
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
return TryGetPath(characterCode, out path);
}
public bool TryGetNormalisedPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
return TryGetPath(characterCode, characterCodeToGlyphId, out path);
}
}
}

View File

@ -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<PdfSubpath> path) => TryGetPath(characterCode, cidToGid.GetGlyphIndex, out path);
public bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
path = null;
if (fontProgram == null)
{
return false;
}
return fontProgram.TryGetPath(characterCode, characterCodeToGlyphId, out path);
}
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
if (!TryGetPath(characterCode, out path))
{
return false;
}
path = FontMatrix.Transform(path).ToList();
return true;
}
public bool TryGetNormalisedPath(int characterCode, Func<int, int?> characterCodeToGlyphId, out IReadOnlyList<PdfSubpath> path)
{
throw new NotImplementedException();
}
}
}

View File

@ -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;
/// <summary>
/// Defines glyphs using a CIDFont
/// </summary>
internal class Type0Font : IFont, IVerticalWritingSupported
{
private readonly CMap ucs2CMap;
// ReSharper disable once NotAccessedField.Local
private readonly bool isChineseJapaneseOrKorean;
private readonly Dictionary<int, CharacterBoundingBox> boundingBoxCache
= new Dictionary<int, CharacterBoundingBox>();
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;
/// <summary>
/// Defines glyphs using a CIDFont
/// </summary>
internal class Type0Font : IFont, IVerticalWritingSupported
{
private readonly CMap ucs2CMap;
// ReSharper disable once NotAccessedField.Local
private readonly bool isChineseJapaneseOrKorean;
private readonly Dictionary<int, CharacterBoundingBox> boundingBoxCache
= new Dictionary<int, CharacterBoundingBox>();
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);
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
return CidFont.TryGetPath(characterCode, out path);
}
/// <inheritdoc/>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
return CidFont.TryGetNormalisedPath(characterCode, out path);
}
}
}

View File

@ -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();
/// <summary>
/// Returns the glyph path for the given character code.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="path">The glyph path for the given character code.</param>
bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path);
/// <summary>
/// Returns the normalised glyph path for the given character code in a PDF.
/// </summary>
/// <param name="characterCode">Character code in a PDF. Not to be confused with unicode.</param>
/// <param name="path">The normalized glyph path for the given character code.</param>
bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path);
}
}

View File

@ -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:

View File

@ -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];
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
if (font == null)
{
path = EmptyArray<PdfSubpath>.Instance;
return false;
}
return font.TryGetPath(characterCode, CharacterCodeToGlyphId, out path);
}
/// <inheritdoc/>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
if (!TryGetPath(characterCode, out path))
{
return false;
}
path = GetFontMatrix().Transform(path).ToList();
return true;
}
}
}

View File

@ -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;
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
path = null;
if (font == null)
{
return false;
}
return font.TryGetPath(characterCode, out path);
}
/// <inheritdoc/>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
if (!TryGetPath(characterCode, out path))
{
return false;
}
path = GetFontMatrix().Transform(path).ToList();
return true;
}
public class MetricOverrides
{
public int? FirstCharacterCode { get; }

View File

@ -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<Type1Font, CompactFontFormatFontCollection> 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;
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
path = null;
IReadOnlyList<PdfSubpath> 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;
}
/// <inheritdoc/>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
if (TryGetPath(characterCode, out path))
{
path = fontMatrix.Transform(path).ToList();
return true;
}
return false;
}
}
}

View File

@ -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;
/// <summary>
@ -120,5 +121,25 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
{
return fontMatrix;
}
/// <summary>
/// <inheritdoc/>
/// <para>Not implemeted.</para>
/// </summary>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
// https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java
path = null;
return false;
}
/// <summary>
/// <inheritdoc/>
/// <para>Not implemeted.</para>
/// </summary>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
return TryGetPath(characterCode, out path);
}
}
}

View File

@ -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;
}
/// <summary>
/// <inheritdoc/>
/// <para>Type 3 fonts do not use vector paths. Always returns <c>false</c>.</para>
/// </summary>
public bool TryGetPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
path = null;
return false;
}
/// <summary>
/// <inheritdoc/>
/// <para>Type 3 fonts do not use vector paths. Always returns <c>false</c>.</para>
/// </summary>
public bool TryGetNormalisedPath(int characterCode, out IReadOnlyList<PdfSubpath> path)
{
return TryGetPath(characterCode, out path);
}
}
}