#6 improve organisation of cff related classes. add failing font test for cff font file. fix bugs with cff parsing

This commit is contained in:
Eliot Jones
2018-11-18 13:53:43 +00:00
parent 8cd2faeb8b
commit 530410c996
19 changed files with 477 additions and 133 deletions

View File

@@ -0,0 +1,41 @@
namespace UglyToad.PdfPig.Tests.Fonts.CompactFontFormat
{
using System;
using System.IO;
using System.Linq;
using PdfPig.Fonts.CompactFontFormat;
using PdfPig.Fonts.CompactFontFormat.Dictionaries;
using Xunit;
public class CompactFontFormatParserTests
{
private readonly CompactFontFormatParser parser = new CompactFontFormatParser(new CompactFontFormatIndividualFontParser(
new CompactFontFormatIndexReader(), new CompactFontFormatTopLevelDictionaryReader(), new CompactFontFormatPrivateDictionaryReader()),
new CompactFontFormatIndexReader());
[Fact]
public void CanReadMinionPro()
{
var fileBytes = GetFileBytes("MinionPro.bin");
var font = parser.Parse(new CompactFontFormatData(fileBytes));
Assert.Equal("MinionPro", font.ToString());
}
private static byte[] GetFileBytes(string name)
{
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Fonts", "CompactFontFormat"));
var files = Directory.GetFiles(documentFolder);
var file = files.FirstOrDefault(x => x.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0);
if (file == null)
{
throw new InvalidOperationException($"Could not find test file {name} in folder {documentFolder}.");
}
return File.ReadAllBytes(file);
}
}
}

View File

@@ -8,10 +8,15 @@
public class IntegrationDocumentTests
{
private static readonly Lazy<string> DocumentFolder = new Lazy<string>(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents")));
[Theory]
[MemberData(nameof(GetAllDocuments))]
public void CanReadAllPages(string documentName)
{
// Add the full path back on, we removed it so we could see it in the test explorer.
documentName = Path.Combine(DocumentFolder.Value, documentName);
using (var document = PdfDocument.Open(documentName, new ParsingOptions{ UseLenientParsing = false}))
{
for (var i = 0; i < document.NumberOfPages; i++)
@@ -25,11 +30,10 @@
{
get
{
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents"));
var files = Directory.GetFiles(DocumentFolder.Value, "*.pdf");
var files = Directory.GetFiles(documentFolder, "*.pdf");
return files.Select(x => new object[] {x});
// Return the shortname so we can see it in the test explorer.
return files.Select(x => new object[] {Path.GetFileName(x)});
}
}
}

View File

@@ -37,7 +37,6 @@
{
var page = document.GetPage(9);
// TODO: This page requires a CFF font parser for Type 1 fonts, see 5.5.1
Assert.Contains("BreedsNative breeds of pig can be found throughout the country. They are a small body size compared to other exotic and crosses pig types. There name varies from region to region, for example", page.Text);
}
}

View File

@@ -38,6 +38,13 @@
</ItemGroup>
<ItemGroup>
<None Remove="Fonts\CompactFontFormat\MinionPro.bin" />
</ItemGroup>
<ItemGroup>
<Content Include="Fonts\CompactFontFormat\MinionPro.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Fonts\Type1\AdobeUtopia.pfa" />
<Content Include="Fonts\Type1\CMBX10.pfa" />
<Content Include="Fonts\Type1\CMBX12.pfa" />

View File

@@ -1,28 +1,46 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Geometry;
using Util.JetBrains.Annotations;
/// <summary>
/// Represents the deferred execution of a Type 2 CharString command.
/// Represents the deferred execution of a Type 2 charstring command.
/// </summary>
internal class LazyType2Command
{
[NotNull]
private readonly Action<Type2BuildCharContext> runCommand;
/// <summary>
/// The name of the command to run. See the Type 2 charstring specification for the possible command names.
/// </summary>
[NotNull]
public string Name { get; }
public LazyType2Command(string name, Action<Type2BuildCharContext> runCommand)
/// <summary>
/// Create a new <see cref="LazyType2Command"/>.
/// </summary>
/// <param name="name">The name of the command.</param>
/// <param name="runCommand">The action to execute when evaluating the command. This modifies the <see cref="Type2BuildCharContext"/>.</param>
public LazyType2Command([NotNull] string name, [NotNull] Action<Type2BuildCharContext> runCommand)
{
Name = name;
Name = name ?? throw new ArgumentNullException(nameof(name));
this.runCommand = runCommand ?? throw new ArgumentNullException(nameof(runCommand));
}
/// <summary>
/// Evaluate the command.
/// </summary>
/// <param name="context">The current <see cref="Type2BuildCharContext"/>.</param>
[DebuggerStepThrough]
public void Run(Type2BuildCharContext context)
public void Run([NotNull] Type2BuildCharContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
runCommand(context);
}
@@ -31,72 +49,4 @@
return Name;
}
}
internal class Type2BuildCharContext
{
private readonly Dictionary<int, decimal> transientArray = new Dictionary<int, decimal>();
public CharStringStack Stack { get; } = new CharStringStack();
public CharacterPath Path { get; } = new CharacterPath();
public PdfPoint CurrentLocation { get; set; } = new PdfPoint(0, 0);
public decimal? Width { get; set; }
public void AddRelativeHorizontalLine(decimal dx)
{
AddRelativeLine(dx, 0);
}
public void AddRelativeVerticalLine(decimal dy)
{
AddRelativeLine(0, dy);
}
public void AddRelativeBezierCurve(decimal dx1, decimal dy1, decimal dx2, decimal dy2, decimal dx3, decimal dy3)
{
var x1 = CurrentLocation.X + dx1;
var y1 = CurrentLocation.Y + dy1;
var x2 = x1 + dx2;
var y2 = y1 + dy2;
var x3 = x2 + dx3;
var y3 = y2 + dy3;
Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
CurrentLocation = new PdfPoint(x3, y3);
}
public void AddRelativeLine(decimal dx, decimal dy)
{
var dest = new PdfPoint(CurrentLocation.X + dx, CurrentLocation.Y + dy);
Path.LineTo(dest.X, dest.Y);
CurrentLocation = dest;
}
public void AddVerticalStemHints(IReadOnlyList<(decimal start, decimal end)> hints)
{
}
public void AddHorizontalStemHints(IReadOnlyList<(decimal start, decimal end)> hints)
{
}
public void AddToTransientArray(decimal value, int location)
{
transientArray[location] = value;
}
public decimal GetFromTransientArray(int location)
{
var result = transientArray[location];
transientArray.Remove(location);
return result;
}
}
}

View File

@@ -0,0 +1,143 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
using System.Collections.Generic;
using Geometry;
/// <summary>
/// The context used and updated when interpreting the commands for a charstring.
/// </summary>
internal class Type2BuildCharContext
{
private readonly Dictionary<int, decimal> transientArray = new Dictionary<int, decimal>();
/// <summary>
/// The local subroutines available in this font.
/// </summary>
public IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> LocalSubroutines { get; }
/// <summary>
/// The global subroutines available in this font set.
/// </summary>
public IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> GlobalSubroutines { get; }
/// <summary>
/// The numbers currently on the Type 2 Build Char stack.
/// </summary>
public CharStringStack Stack { get; } = new CharStringStack();
/// <summary>
/// The current path.
/// </summary>
public CharacterPath Path { get; } = new CharacterPath();
/// <summary>
/// The current location of the active point.
/// </summary>
public PdfPoint CurrentLocation { get; set; } = new PdfPoint(0, 0);
/// <summary>
/// If the charstring has a width other than that of defaultWidthX it must be specified as the first
/// number in the charstring, and encoded as the difference from nominalWidthX.
/// </summary>
public decimal? Width { get; set; }
/// <summary>
/// Create a new <see cref="Type2BuildCharContext"/>.
/// </summary>
public Type2BuildCharContext(IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
IReadOnlyDictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
{
LocalSubroutines = localSubroutines;
GlobalSubroutines = globalSubroutines;
}
public void AddRelativeHorizontalLine(decimal dx)
{
AddRelativeLine(dx, 0);
}
public void AddRelativeVerticalLine(decimal dy)
{
AddRelativeLine(0, dy);
}
public void AddRelativeBezierCurve(decimal dx1, decimal dy1, decimal dx2, decimal dy2, decimal dx3, decimal dy3)
{
var x1 = CurrentLocation.X + dx1;
var y1 = CurrentLocation.Y + dy1;
var x2 = x1 + dx2;
var y2 = y1 + dy2;
var x3 = x2 + dx3;
var y3 = y2 + dy3;
Path.BezierCurveTo(x1, y1, x2, y2, x3, y3);
CurrentLocation = new PdfPoint(x3, y3);
}
public void AddRelativeLine(decimal dx, decimal dy)
{
var dest = new PdfPoint(CurrentLocation.X + dx, CurrentLocation.Y + dy);
Path.LineTo(dest.X, dest.Y);
CurrentLocation = dest;
}
public void AddVerticalStemHints(IReadOnlyList<(decimal start, decimal end)> hints)
{
}
public void AddHorizontalStemHints(IReadOnlyList<(decimal start, decimal end)> hints)
{
}
public void AddToTransientArray(decimal value, int location)
{
transientArray[location] = value;
}
public decimal GetFromTransientArray(int location)
{
var result = transientArray[location];
transientArray.Remove(location);
return result;
}
public int GetLocalSubroutineBias()
{
var count = LocalSubroutines.Count;
return CountToBias(count);
}
public int GetGlobalSubroutineBias()
{
var count = GlobalSubroutines.Count;
return CountToBias(count);
}
private static int CountToBias(int count)
{
if (count < 1240)
{
return 107;
}
if (count < 33900)
{
return 1131;
}
return 32768;
}
public void EvaluateSubroutine(Type2CharStrings.CommandSequence subroutine)
{
foreach (var command in subroutine.Commands)
{
command.Match(x => Stack.Push(x),
act => act.Run(this));
}
}
}
}

View File

@@ -189,7 +189,14 @@
ctx.Stack.Clear();
})
},
{ 10, new LazyType2Command("callsubr", x => { })},
{ 10, new LazyType2Command("callsubr", ctx =>
{
var index = (int)ctx.Stack.PopTop();
var bias = ctx.GetLocalSubroutineBias();
var actualIndex = index + bias;
var subr = ctx.LocalSubroutines[actualIndex];
ctx.EvaluateSubroutine(subr);
})},
{ 11, new LazyType2Command("return", x => { })},
{ 14, new LazyType2Command("endchar", ctx =>
{
@@ -379,7 +386,15 @@
ctx.Stack.Clear();
})
},
{ 29, new LazyType2Command("callgsubr", x => { })},
{ 29, new LazyType2Command("callgsubr", ctx =>
{
var index = (int)ctx.Stack.PopTop();
var bias = ctx.GetLocalSubroutineBias();
var actualIndex = index + bias;
var subr = ctx.LocalSubroutines[actualIndex];
ctx.EvaluateSubroutine(subr);
})
},
{ 30,
new LazyType2Command("vhcurveto", ctx =>
{
@@ -696,6 +711,22 @@
throw new ArgumentNullException(nameof(globalSubroutines));
}
var localSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
for (var i = 0; i < localSubroutines.Count; i++)
{
var bytes = localSubroutines[i];
var sequence = ParseSingle(bytes);
localSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
}
var globalSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
for (var i = 0; i < globalSubroutines.Count; i++)
{
var bytes = globalSubroutines[i];
var sequence = ParseSingle(bytes);
globalSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
}
var charStrings = new Dictionary<string, Type2CharStrings.CommandSequence>();
for (var i = 0; i < charStringBytes.Count; i++)
{
@@ -704,7 +735,7 @@
charStrings[charset.GetNameByGlyphId(i)] = new Type2CharStrings.CommandSequence(sequence);
}
return new Type2CharStrings(charStrings, new Dictionary<int, Type2CharStrings.CommandSequence>());
return new Type2CharStrings(charStrings, localSubroutineSequences, globalSubroutineSequences);
}
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(IReadOnlyList<byte> bytes)
@@ -793,31 +824,40 @@
*/
var stemCount = 0;
var precedingNumbers = 0;
var hasEncounteredInitialHintMask = false;
for (var j = 0; j < precedingCommands.Count; j++)
{
var item = precedingCommands[j];
item.Match(x => precedingNumbers++,
x =>
{
// The numbers preceding the first hintmask following hinting can act as vertical hints.
if (x.Name == "hintmask" && !hasEncounteredInitialHintMask)
{
hasEncounteredInitialHintMask = true;
stemCount += precedingNumbers / 2;
return;
}
if (!HintingCommandNames.Contains(x.Name))
{
precedingNumbers = 0;
return;
}
// ReSharper disable once AccessToModifiedClosure
stemCount += precedingNumbers / 2;
precedingNumbers = 0;
});
}
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)
if (precedingNumbers > 0 && !hasEncounteredInitialHintMask)
{
stemCount += precedingNumbers / 2;
fullStemCount += precedingNumbers / 2;
}
var minimumFullBytes = Math.Ceiling(stemCount / 8d);
var minimumFullBytes = Math.Ceiling(fullStemCount / 8d);
// Skip the following hintmask or cntrmask data bytes
i += (int)minimumFullBytes;
}

View File

@@ -5,6 +5,11 @@
using System.Linq;
using Util;
/// <summary>
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
/// as the local (per font) and global (per font set) subroutines.
/// The CharStrings are lazily evaluated.
/// </summary>
internal class Type2CharStrings
{
private readonly object locker = new object();
@@ -12,14 +17,22 @@
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
public IReadOnlyDictionary<int, CommandSequence> Subroutines { get; }
public IReadOnlyDictionary<int, CommandSequence> LocalSubroutines { get; }
public IReadOnlyDictionary<int, CommandSequence> GlobalSubroutines { get; }
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, IReadOnlyDictionary<int, CommandSequence> subroutines)
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, IReadOnlyDictionary<int, CommandSequence> localSubroutines,
IReadOnlyDictionary<int, CommandSequence> globalSubroutines)
{
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
Subroutines = subroutines ?? throw new ArgumentNullException(nameof(subroutines));
LocalSubroutines = localSubroutines ?? throw new ArgumentNullException(nameof(localSubroutines));
GlobalSubroutines = globalSubroutines ?? throw new ArgumentNullException(nameof(globalSubroutines));
}
/// <summary>
/// Evaluate the CharString for the character with a given name returning the path constructed for the glyph.
/// </summary>
/// <param name="name">The name of the character to retrieve the CharString for.</param>
/// <returns>A <see cref="CharacterPath"/> for the glyph.</returns>
public CharacterPath Generate(string name)
{
CharacterPath glyph;
@@ -43,9 +56,9 @@
return glyph;
}
public static CharacterPath Run(CommandSequence sequence)
private CharacterPath Run(CommandSequence sequence)
{
var context = new Type2BuildCharContext();
var context = new Type2BuildCharContext(LocalSubroutines, GlobalSubroutines);
var hasRunStackClearingCommand = false;
foreach (var command in sequence.Commands)

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using System.Diagnostics;
using System.Text;
/// <summary>
@@ -12,6 +13,9 @@
public int Position { get; private set; } = -1;
public int Length => dataBytes.Length;
[DebuggerStepThrough]
public CompactFontFormatData(byte[] dataBytes)
{
this.dataBytes = dataBytes;
@@ -104,5 +108,17 @@
return result;
}
public CompactFontFormatData SnapshotPortion(int startLocation, int length)
{
if (startLocation > dataBytes.Length - 1 || startLocation + length > dataBytes.Length)
{
throw new ArgumentException($"Attempted to create a snapshot of an invalid portion of the data. Length was {dataBytes.Length}, requested start: {startLocation} and requested length: {length}.");
}
var newData = new byte[length];
Array.Copy(dataBytes, startLocation, newData, 0, length);
return new CompactFontFormatData(newData);
}
}
}

View File

@@ -1,14 +1,16 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using Charsets;
using CharStrings;
using Dictionaries;
using Geometry;
using Type1.CharStrings;
using Util;
internal class CompactFontFormatFont
{
private readonly CompactFontFormatTopLevelDictionary topDictionary;
public CompactFontFormatTopLevelDictionary TopDictionary { get; }
private readonly CompactFontFormatPrivateDictionary privateDictionary;
private readonly ICompactFontFormatCharset charset;
private readonly Union<Type1CharStrings, Type2CharStrings> charStrings;
@@ -17,10 +19,19 @@
ICompactFontFormatCharset charset,
Union<Type1CharStrings, Type2CharStrings> charStrings)
{
this.topDictionary = topDictionary;
TopDictionary = topDictionary;
this.privateDictionary = privateDictionary;
this.charset = charset;
this.charStrings = charStrings;
}
public PdfRectangle? GetCharacterBoundingBox(string characterName)
{
var result = default(PdfRectangle?);
charStrings.Match(x => throw new NotImplementedException("Type 1 CharStrings in a CFF font are currently unsupported."),
x => { result = x.Generate(characterName).GetBoundingRectangle(); });
return result;
}
}
}

View File

@@ -0,0 +1,47 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using System.Collections.Generic;
using System.Linq;
using Core;
using Geometry;
internal class CompactFontFormatFontSet
{
private readonly CompactFontFormatHeader header;
private readonly IReadOnlyList<string> fontNames;
private readonly IReadOnlyDictionary<string, CompactFontFormatFont> fontSet;
public CompactFontFormatFontSet(CompactFontFormatHeader header, IReadOnlyList<string> fontNames,
IReadOnlyDictionary<string, CompactFontFormatFont> fontSet)
{
this.header = header;
this.fontNames = fontNames;
this.fontSet = fontSet;
}
public TransformationMatrix GetFontTransformationMatrix()
{
var result = GetFont().TopDictionary.FontMatrix;
return result;
}
public PdfRectangle? GetCharacterBoundingBox(string characterName)
{
var font = GetFont();
return font.GetCharacterBoundingBox(characterName);
}
private CompactFontFormatFont GetFont()
{
#if DEBUG
// TODO: what to do if there are multiple fonts?
if (fontSet.Count > 1)
{
throw new NotSupportedException("Multiple fonts in a CFF");
}
#endif
return fontSet.First().Value;
}
}
}

View File

@@ -21,6 +21,11 @@ namespace UglyToad.PdfPig.Fonts.CompactFontFormat
throw new InvalidOperationException($"Negative object length {length} at {i}. Current position: {data.Position}.");
}
if (length > data.Length)
{
throw new InvalidOperationException($"Attempted to read data of length {length} in data array of length {data.Length}.");
}
results[i] = data.ReadBytes(length);
}

View File

@@ -36,11 +36,12 @@
var privateDictionary = CompactFontFormatPrivateDictionary.GetDefault();
if (topDictionary.PrivateDictionarySizeAndOffset.Item2 >= 0)
if (topDictionary.PrivateDictionaryLocation.HasValue)
{
data.Seek(topDictionary.PrivateDictionarySizeAndOffset.Item2);
var privateDictionaryBytes = data.SnapshotPortion(topDictionary.PrivateDictionaryLocation.Value.Offset,
topDictionary.PrivateDictionaryLocation.Value.Size);
privateDictionary = privateDictionaryReader.Read(data, stringIndex);
privateDictionary = privateDictionaryReader.Read(privateDictionaryBytes, stringIndex);
}
if (topDictionary.CharStringsOffset < 0)
@@ -49,9 +50,9 @@
}
var localSubroutines = CompactFontFormatIndex.None;
if (privateDictionary.LocalSubroutineOffset.HasValue)
if (privateDictionary.LocalSubroutineOffset.HasValue && topDictionary.PrivateDictionaryLocation.HasValue)
{
data.Seek(privateDictionary.LocalSubroutineOffset.Value);
data.Seek(privateDictionary.LocalSubroutineOffset.Value + topDictionary.PrivateDictionaryLocation.Value.Offset);
localSubroutines = indexReader.ReadDictionaryData(data);
}
@@ -154,9 +155,16 @@
var builder = new StringBuilder("<!DOCTYPE html><html><head></head><body>");
foreach (var pair in charStrings.CharStrings)
{
var path = Type2CharStrings.Run(pair.Value);
var svg = path.ToFullSvg();
builder.AppendLine(svg);
try
{
var path = charStrings.Generate(pair.Key);
var svg = path.ToFullSvg();
builder.AppendLine(svg);
}
catch (Exception ex)
{
}
}
builder.Append("</body></html>");

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using System.Collections.Generic;
using Util;
internal class CompactFontFormatParser
@@ -18,18 +19,19 @@
this.indexReader = indexReader;
}
public void Parse(CompactFontFormatData data)
public CompactFontFormatFontSet Parse(CompactFontFormatData data)
{
var tag = ReadTag(data);
switch (tag)
{
// An OpenType font containing CFF data.
case TagOtto:
throw new NotSupportedException("Currently tagged CFF data is not supported.");
case TagTtcf:
throw new NotSupportedException("True Type Collection fonts are not supported.");
throw new NotSupportedException("True Type Collection fonts are not currently supported.");
case TagTtfonly:
throw new NotSupportedException("OpenType fonts containing a true type font are not supported.");
throw new NotSupportedException("OpenType fonts containing a true type font are not currently supported.");
default:
data.Seek(0);
break;
@@ -39,18 +41,22 @@
var fontNames = ReadStringIndex(data);
var topLevelDict = indexReader.ReadDictionaryData(data);
var topLevelDictionaryIndex = indexReader.ReadDictionaryData(data);
var stringIndex = ReadStringIndex(data);
var globalSubroutineIndex = indexReader.ReadDictionaryData(data);
var fonts = new Dictionary<string, CompactFontFormatFont>();
for (var i = 0; i < fontNames.Length; i++)
{
var fontName = fontNames[i];
individualFontParser.Parse(data, fontName, topLevelDict[i], stringIndex, globalSubroutineIndex);
fonts[fontName] = individualFontParser.Parse(data, fontName, topLevelDictionaryIndex[i], stringIndex, globalSubroutineIndex);
}
return new CompactFontFormatFontSet(header, fontNames, fonts);
}
private static string ReadTag(CompactFontFormatData data)

View File

@@ -1,6 +1,5 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.Dictionaries
{
using System;
using Core;
using Geometry;
@@ -46,12 +45,7 @@
public int EncodingOffset { get; set; } = UnsetOffset;
private Tuple<int, int> privateDictionarySizeAndOffset = Tuple.Create(0, UnsetOffset);
public Tuple<int, int> PrivateDictionarySizeAndOffset
{
get => privateDictionarySizeAndOffset ?? Tuple.Create(0, UnsetOffset);
set => privateDictionarySizeAndOffset = value;
}
public SizeAndOffset? PrivateDictionaryLocation { get; set; }
public int CharStringsOffset { get; set; } = -1;
@@ -64,6 +58,24 @@
public decimal[] BaseFontBlend { get; set; }
public bool IsCidFont { get; set; }
public struct SizeAndOffset
{
public int Size { get; }
public int Offset { get; }
public SizeAndOffset(int size, int offset)
{
Size = size;
Offset = offset;
}
public override string ToString()
{
return $"Size: {Size}, Offset: {Offset}";
}
}
}
/// <summary>

View File

@@ -136,7 +136,7 @@
var size = GetIntOrDefault(operands);
operands.RemoveAt(0);
var offset = GetIntOrDefault(operands);
dictionary.PrivateDictionarySizeAndOffset = Tuple.Create(size, offset);
dictionary.PrivateDictionaryLocation = new CompactFontFormatTopLevelDictionary.SizeAndOffset(size, offset);
}
break;
}

View File

@@ -1,6 +1,5 @@
namespace UglyToad.PdfPig.Fonts.Parser.Handlers
{
using System;
using Cmap;
using CompactFontFormat;
using Encodings;
@@ -14,6 +13,7 @@
using Tokens;
using Type1;
using Type1.Parser;
using Util;
internal class Type1FontHandler : IFontHandler
{
@@ -66,7 +66,7 @@
var descriptor = FontDictionaryAccessHelper.GetFontDescriptor(pdfScanner, fontDescriptorFactory, dictionary, isLenientParsing);
var font = ParseType1Font(descriptor, isLenientParsing);
var font = ParseFontProgram(descriptor, isLenientParsing);
var name = FontDictionaryAccessHelper.GetName(pdfScanner, dictionary, descriptor, isLenientParsing);
@@ -85,15 +85,15 @@
Encoding encoding = encodingReader.Read(dictionary, isLenientParsing, descriptor);
if (encoding == null && font?.Encoding.Count > 0)
if (encoding == null)
{
encoding = new BuiltInEncoding(font.Encoding);
font?.Match(x => encoding = new BuiltInEncoding(x.Encoding), _ => {});
}
return new Type1FontSimple(name, firstCharacter, lastCharacter, widths, descriptor, encoding, toUnicodeCMap, font);
}
private Type1FontProgram ParseType1Font(FontDescriptor descriptor, bool isLenientParsing)
private Union<Type1FontProgram, CompactFontFormatFontSet> ParseFontProgram(FontDescriptor descriptor, bool isLenientParsing)
{
if (descriptor?.FontFile == null)
{
@@ -118,9 +118,8 @@
if (stream.StreamDictionary.TryGet(NameToken.Subtype, out NameToken subTypeName)
&& NameToken.Type1C.Equals(subTypeName))
{
compactFontFormatParser.Parse(new CompactFontFormatData(bytes));
throw new NotSupportedException("TODO: support Compact Font Format...");
return null;
var cffFont = compactFontFormatParser.Parse(new CompactFontFormatData(bytes));
return Union<Type1FontProgram, CompactFontFormatFontSet>.Two(cffFont);
}
var length1 = stream.StreamDictionary.Get<NumericToken>(NameToken.Length1, pdfScanner);
@@ -128,7 +127,7 @@
var font = type1FontParser.Parse(new ByteArrayInputBytes(bytes), length1.Int, length2.Int);
return font;
return Union<Type1FontProgram, CompactFontFormatFontSet>.One(font);
}
catch
{

View File

@@ -1,6 +1,8 @@
namespace UglyToad.PdfPig.Fonts.Simple
{
using System;
using Cmap;
using CompactFontFormat;
using Composite;
using Core;
using Encodings;
@@ -8,6 +10,8 @@
using IO;
using Tokens;
using Type1;
using Util;
using Util.JetBrains.Annotations;
/// <summary>
/// A font based on the Adobe Type 1 font format.
@@ -23,7 +27,9 @@
private readonly FontDescriptor fontDescriptor;
private readonly Encoding encoding;
private readonly Type1FontProgram fontProgram;
[CanBeNull]
private readonly Union<Type1FontProgram, CompactFontFormatFontSet> fontProgram;
private readonly ToUnicodeCMap toUnicodeCMap;
@@ -35,7 +41,7 @@
public Type1FontSimple(NameToken name, int firstChar, int lastChar, decimal[] widths, FontDescriptor fontDescriptor, Encoding encoding,
CMap toUnicodeCMap,
Type1FontProgram fontProgram)
Union<Type1FontProgram, CompactFontFormatFontSet> fontProgram)
{
this.firstChar = firstChar;
this.lastChar = lastChar;
@@ -71,15 +77,18 @@
}
catch
{
if (fontProgram != null)
if (fontProgram == null)
{
var result = fontProgram.Encoding.TryGetValue(characterCode, out value);
return result;
return false;
}
// our quick hack has failed, we should decode the type 1 font!
}
return false;
var containsEncoding = false;
var capturedValue = default(string);
fontProgram.Match(x => { containsEncoding = x.Encoding.TryGetValue(characterCode, out capturedValue); },
_ => {});
value = capturedValue;
return containsEncoding;
}
}
var name = encoding.GetName(characterCode);
@@ -100,11 +109,27 @@
{
var boundingBox = GetBoundingBoxInGlyphSpace(characterCode);
boundingBox = fontMatrix.Transform(boundingBox);
var matrix = GetFontMatrixInternal();
boundingBox = matrix.Transform(boundingBox);
return new CharacterBoundingBox(boundingBox, boundingBox);
}
private TransformationMatrix GetFontMatrixInternal()
{
if (fontProgram == null)
{
return fontMatrix;
}
var matrix = default(TransformationMatrix);
fontProgram.Match(x => { matrix = fontMatrix; }, x => { matrix = x.GetFontTransformationMatrix(); });
return matrix;
}
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
{
if (characterCode < firstChar || characterCode > lastChar)
@@ -117,13 +142,31 @@
return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0);
}
var rect = fontProgram.GetCharacterBoundingBox(characterCode);
var rect = default(PdfRectangle?);
fontProgram.Match(x =>
{
rect = x.GetCharacterBoundingBox(characterCode);
},
x =>
{
string characterName;
if (encoding != null)
{
characterName = encoding.GetName(characterCode);
}
else
{
throw new NotImplementedException("Unclear how to access the character name for CFF fonts when no encoding is present.");
}
rect = x.GetCharacterBoundingBox(characterName);
});
if (!rect.HasValue)
{
return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0);
}
// ReSharper disable once PossibleInvalidOperationException
return rect.Value;
}