mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-15 03:34:52 +08:00
#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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/UglyToad.PdfPig.Tests/Fonts/CompactFontFormat/MinionPro.bin
Normal file
BIN
src/UglyToad.PdfPig.Tests/Fonts/CompactFontFormat/MinionPro.bin
Normal file
Binary file not shown.
@@ -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)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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" />
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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>");
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user