#6 complete parsing of type 2 charstrings. subroutines now eager evaluated from raw bytes

This commit is contained in:
Eliot Jones
2018-11-19 21:23:21 +00:00
parent 6517a6201a
commit 8cd05807ca
3 changed files with 70 additions and 186 deletions

View File

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

View File

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

View File

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