mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-15 11:44:51 +08:00
#6 complete parsing of type 2 charstrings. subroutines now eager evaluated from raw bytes
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Geometry;
|
||||
using Util;
|
||||
|
||||
/// <summary>
|
||||
/// The context used and updated when interpreting the commands for a charstring.
|
||||
@@ -10,17 +9,7 @@
|
||||
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>
|
||||
@@ -42,18 +31,6 @@
|
||||
/// </summary>
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
public List<Union<decimal, LazyType2Command>> All { get; } = new List<Union<decimal, LazyType2Command>>();
|
||||
|
||||
/// <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);
|
||||
@@ -107,18 +84,6 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
public int GetLocalSubroutineBias()
|
||||
{
|
||||
var count = LocalSubroutines.Count;
|
||||
return CountToBias(count);
|
||||
}
|
||||
|
||||
public int GetGlobalSubroutineBias()
|
||||
{
|
||||
var count = GlobalSubroutines.Count;
|
||||
return CountToBias(count);
|
||||
}
|
||||
|
||||
public static int CountToBias(int count)
|
||||
{
|
||||
if (count < 1240)
|
||||
@@ -133,15 +98,5 @@
|
||||
|
||||
return 32768;
|
||||
}
|
||||
|
||||
public void EvaluateSubroutine(Type2CharStrings.CommandSequence subroutine)
|
||||
{
|
||||
foreach (var command in subroutine.Commands)
|
||||
{
|
||||
All.Add(command);
|
||||
command.Match(x => Stack.Push(x),
|
||||
act => act.Run(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -190,15 +190,8 @@
|
||||
ctx.Stack.Clear();
|
||||
})
|
||||
},
|
||||
{ 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 => { })},
|
||||
{ 10, new LazyType2Command("callsubr", ctx => {})},
|
||||
{ 11, new LazyType2Command("return", ctx => {})},
|
||||
{ 14, new LazyType2Command("endchar", ctx =>
|
||||
{
|
||||
ctx.Stack.Clear();
|
||||
@@ -387,14 +380,7 @@
|
||||
ctx.Stack.Clear();
|
||||
})
|
||||
},
|
||||
{ 29, new LazyType2Command("callgsubr", ctx =>
|
||||
{
|
||||
var index = (int)ctx.Stack.PopTop();
|
||||
var bias = ctx.GetGlobalSubroutineBias();
|
||||
var actualIndex = index + bias;
|
||||
var subr = ctx.GlobalSubroutines[actualIndex];
|
||||
ctx.EvaluateSubroutine(subr);
|
||||
})
|
||||
{ 29, new LazyType2Command("callgsubr", ctx => {})
|
||||
},
|
||||
{ 30,
|
||||
new LazyType2Command("vhcurveto", ctx =>
|
||||
@@ -711,38 +697,22 @@
|
||||
{
|
||||
throw new ArgumentNullException(nameof(globalSubroutines));
|
||||
}
|
||||
|
||||
var globalSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
|
||||
for (var i = 0; i < globalSubroutines.Count; i++)
|
||||
{
|
||||
var bytes = globalSubroutines[i];
|
||||
var sequence = ParseSingle(bytes, null, null);
|
||||
globalSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
|
||||
}
|
||||
|
||||
var localSubroutineSequences = new Dictionary<int, Type2CharStrings.CommandSequence>();
|
||||
for (var i = 0; i < localSubroutines.Count; i++)
|
||||
{
|
||||
var bytes = localSubroutines[i];
|
||||
var sequence = ParseSingle(bytes, null, globalSubroutineSequences);
|
||||
localSubroutineSequences[i] = new Type2CharStrings.CommandSequence(sequence);
|
||||
}
|
||||
|
||||
|
||||
var charStrings = new Dictionary<string, Type2CharStrings.CommandSequence>();
|
||||
for (var i = 0; i < charStringBytes.Count; i++)
|
||||
{
|
||||
var charString = charStringBytes[i];
|
||||
var name = charset.GetNameByGlyphId(i);
|
||||
var sequence = ParseSingle(charString, localSubroutineSequences, globalSubroutineSequences);
|
||||
var sequence = ParseSingle(charString.ToList(), localSubroutines, globalSubroutines);
|
||||
charStrings[name] = new Type2CharStrings.CommandSequence(sequence);
|
||||
}
|
||||
|
||||
return new Type2CharStrings(charStrings, localSubroutineSequences, globalSubroutineSequences);
|
||||
return new Type2CharStrings(charStrings, localSubroutines, globalSubroutines);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(IReadOnlyList<byte> bytes,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
||||
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(List<byte> bytes,
|
||||
CompactFontFormatIndex localSubroutines,
|
||||
CompactFontFormatIndex globalSubroutines)
|
||||
{
|
||||
var instructions = new List<Union<decimal, LazyType2Command>>();
|
||||
for (var i = 0; i < bytes.Count; i++)
|
||||
@@ -751,7 +721,10 @@
|
||||
if (b <= 31 && b != 28)
|
||||
{
|
||||
var command = GetCommand(b, bytes, instructions, localSubroutines, globalSubroutines, ref i);
|
||||
instructions.Add(Union<decimal, LazyType2Command>.Two(command));
|
||||
if (command != null)
|
||||
{
|
||||
instructions.Add(Union<decimal, LazyType2Command>.Two(command));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -803,10 +776,10 @@
|
||||
return lead + (fractionalPart / 65535m);
|
||||
}
|
||||
|
||||
private static LazyType2Command GetCommand(byte b, IReadOnlyList<byte> bytes,
|
||||
IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines, ref int i)
|
||||
private static LazyType2Command GetCommand(byte b, List<byte> bytes,
|
||||
List<Union<decimal, LazyType2Command>> precedingCommands,
|
||||
CompactFontFormatIndex localSubroutines,
|
||||
CompactFontFormatIndex globalSubroutines, ref int i)
|
||||
{
|
||||
if (b == 12)
|
||||
{
|
||||
@@ -819,28 +792,53 @@
|
||||
return new LazyType2Command($"unknown: {b} {b2}", x => { });
|
||||
}
|
||||
|
||||
// hintmask and cntrmask
|
||||
if (b == 19 || b == 20)
|
||||
// Invoke a subroutine, substitute the subroutine bytes into this sequence.
|
||||
if (b == 10 || b == 29)
|
||||
{
|
||||
var minimumFullBytes = CalculatePrecedingHintBytes(precedingCommands, localSubroutines, globalSubroutines);
|
||||
var isLocal = b == 10;
|
||||
int precedingNumber = 0;
|
||||
precedingCommands[precedingCommands.Count - 1].Match(x => precedingNumber = (int)x,
|
||||
_ => throw new InvalidOperationException("A subroutine call must be preceded by a number, not a command."));
|
||||
|
||||
var bias = Type2BuildCharContext.CountToBias(isLocal ? localSubroutines.Count : globalSubroutines.Count);
|
||||
var index = precedingNumber + bias;
|
||||
var subroutineBytes = isLocal ? localSubroutines[index] : globalSubroutines[index];
|
||||
bytes.RemoveRange(i - 1, 2);
|
||||
bytes.InsertRange(i - 1, subroutineBytes);
|
||||
|
||||
// Remove the subroutine index
|
||||
precedingCommands.RemoveAt(precedingCommands.Count - 1);
|
||||
i -= 2;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (b == 19 || b == 20)
|
||||
{
|
||||
// hintmask and cntrmask
|
||||
var minimumFullBytes = CalculatePrecedingHintBytes(precedingCommands);
|
||||
// Skip the following hintmask or cntrmask data bytes
|
||||
i += minimumFullBytes;
|
||||
}
|
||||
|
||||
if (SingleByteCommandStore.TryGetValue(b, out var command))
|
||||
{
|
||||
// Ignore return
|
||||
if (command.Name == "return")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
return new LazyType2Command($"unknown: {b}", x => { });
|
||||
}
|
||||
|
||||
private static int CalculatePrecedingHintBytes(IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
||||
private static int CalculatePrecedingHintBytes(IReadOnlyList<Union<decimal, LazyType2Command>> precedingCommands)
|
||||
{
|
||||
int SafeStemCount(int counts)
|
||||
{
|
||||
// Where there an odd number of stem arguments take only as many as the even number requires.
|
||||
if (counts % 2 == 0)
|
||||
{
|
||||
return counts / 2;
|
||||
@@ -848,8 +846,6 @@
|
||||
|
||||
return (counts - 1) / 2;
|
||||
}
|
||||
// Do a first pass to substitute in all local and global subroutines prior to the first hintmask.
|
||||
var commandsToCountHints = BuildFullCommandSequence(precedingCommands, localSubroutines, globalSubroutines);
|
||||
|
||||
/*
|
||||
* The hintmask operator is followed by one or more data bytes that specify the stem hints which are to be active for the
|
||||
@@ -860,9 +856,9 @@
|
||||
var stemCount = 0;
|
||||
var precedingNumbers = 0;
|
||||
var hasEncounteredInitialHintMask = false;
|
||||
for (var j = 0; j < commandsToCountHints.Count; j++)
|
||||
for (var j = 0; j < precedingCommands.Count; j++)
|
||||
{
|
||||
var item = commandsToCountHints[j];
|
||||
var item = precedingCommands[j];
|
||||
item.Match(x =>
|
||||
{
|
||||
precedingNumbers++;
|
||||
@@ -904,82 +900,5 @@
|
||||
|
||||
return minimumFullBytes;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Union<decimal, LazyType2Command>> BuildFullCommandSequence(IReadOnlyList<Union<decimal, LazyType2Command>> allNonSubroutineCommands,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> localSubroutines,
|
||||
Dictionary<int, Type2CharStrings.CommandSequence> globalSubroutines)
|
||||
{
|
||||
/*
|
||||
* Since the initial hintmask or stem information may be in a subroutine we must include subroutines
|
||||
* when calculating the overall number of hints. This is a nuisance because we have to evaluate the
|
||||
* charstring but should be rare enough to avoid a performance hit.
|
||||
*/
|
||||
bool IsSubroutine(LazyType2Command cmd)
|
||||
{
|
||||
return cmd.Name == "callsubr" || cmd.Name == "callgsubr";
|
||||
}
|
||||
|
||||
if (localSubroutines == null || globalSubroutines == null)
|
||||
{
|
||||
return allNonSubroutineCommands;
|
||||
}
|
||||
|
||||
// Build a mutable list of the commands to substitute subroutine into.
|
||||
var results = new List<Union<decimal, LazyType2Command>>(allNonSubroutineCommands);
|
||||
|
||||
var firstHintmask = false;
|
||||
|
||||
var previousNumber = -1m;
|
||||
for (var i = 0; i < results.Count; i++)
|
||||
{
|
||||
var command = results[i];
|
||||
|
||||
var foundSubroutine = false;
|
||||
var wasLocalSubroutine = false;
|
||||
var subroutineIndex = 0;
|
||||
|
||||
command.Match(x => previousNumber = x, x =>
|
||||
{
|
||||
// When we have a subroutine which appears before a hintmask we substitute the actual commands in to check for hints.
|
||||
if (IsSubroutine(x) && !firstHintmask)
|
||||
{
|
||||
foundSubroutine = true;
|
||||
wasLocalSubroutine = x.Name == "callsubr";
|
||||
|
||||
var bias = Type2BuildCharContext.CountToBias(wasLocalSubroutine ? localSubroutines.Count : globalSubroutines.Count);
|
||||
subroutineIndex = (int) previousNumber + bias;
|
||||
}
|
||||
else if (x.Name == "hintmask")
|
||||
{
|
||||
firstHintmask = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (foundSubroutine)
|
||||
{
|
||||
// Replace the call to the local or global subroutine with the actual routine.
|
||||
var routine = wasLocalSubroutine ? localSubroutines[subroutineIndex] : globalSubroutines[subroutineIndex];
|
||||
results.RemoveRange(i-1, 2);
|
||||
|
||||
// Skip any return commands since they interfere with counting.
|
||||
results.InsertRange(i-1 , routine.Commands.Where(x =>
|
||||
{
|
||||
var isReturn = false;
|
||||
x.Match(_ => {}, y => isReturn = y.Name == "return");
|
||||
return !isReturn;
|
||||
}));
|
||||
|
||||
i -= 2;
|
||||
}
|
||||
|
||||
// Exit once we hit the first hintmask since all hints have now been declared.
|
||||
if (firstHintmask)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Util;
|
||||
using Util.JetBrains.Annotations;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the decoded command sequences for Type 2 CharStrings from a Compact Font Format font as well
|
||||
@@ -15,13 +16,25 @@
|
||||
private readonly object locker = new object();
|
||||
private readonly Dictionary<string, CharacterPath> glyphs = new Dictionary<string, CharacterPath>();
|
||||
|
||||
/// <summary>
|
||||
/// The decoded charstrings in this font.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, CommandSequence> CharStrings { get; }
|
||||
|
||||
public IReadOnlyDictionary<int, CommandSequence> LocalSubroutines { get; }
|
||||
public IReadOnlyDictionary<int, CommandSequence> GlobalSubroutines { get; }
|
||||
/// <summary>
|
||||
/// The indexed bytes for the local subroutines in this font.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public CompactFontFormatIndex LocalSubroutines { get; }
|
||||
|
||||
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, IReadOnlyDictionary<int, CommandSequence> localSubroutines,
|
||||
IReadOnlyDictionary<int, CommandSequence> globalSubroutines)
|
||||
/// <summary>
|
||||
/// The indexed bytes for the global subroutines in this font set.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public CompactFontFormatIndex GlobalSubroutines { get; }
|
||||
|
||||
public Type2CharStrings(IReadOnlyDictionary<string, CommandSequence> charStrings, CompactFontFormatIndex localSubroutines,
|
||||
CompactFontFormatIndex globalSubroutines)
|
||||
{
|
||||
CharStrings = charStrings ?? throw new ArgumentNullException(nameof(charStrings));
|
||||
LocalSubroutines = localSubroutines ?? throw new ArgumentNullException(nameof(localSubroutines));
|
||||
@@ -52,8 +65,6 @@
|
||||
{
|
||||
glyph = Run(sequence);
|
||||
|
||||
var svg = glyph.ToFullSvg();
|
||||
|
||||
glyphs[name] = glyph;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -65,14 +76,13 @@
|
||||
return glyph;
|
||||
}
|
||||
|
||||
private CharacterPath Run(CommandSequence sequence)
|
||||
private static CharacterPath Run(CommandSequence sequence)
|
||||
{
|
||||
var context = new Type2BuildCharContext(LocalSubroutines, GlobalSubroutines);
|
||||
var context = new Type2BuildCharContext();
|
||||
|
||||
var hasRunStackClearingCommand = false;
|
||||
foreach (var command in sequence.Commands)
|
||||
{
|
||||
context.All.Add(command);
|
||||
command.Match(x => context.Stack.Push(x),
|
||||
x =>
|
||||
{
|
||||
|
Reference in New Issue
Block a user