tidy up cff font classes and start adding type 2 charstring support for #6

This commit is contained in:
Eliot Jones
2018-11-16 21:30:59 +00:00
parent 946ad1aec6
commit 81935d5cdb
11 changed files with 465 additions and 170 deletions

View File

@@ -0,0 +1,78 @@
namespace UglyToad.PdfPig.Fonts
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// The stack of numeric operands currently active in a CharString.
/// </summary>
internal class CharStringStack
{
private readonly List<decimal> stack = new List<decimal>();
/// <summary>
/// The current size of the stack.
/// </summary>
public int Length => stack.Count;
/// <summary>
/// Whether it's possible to pop a value from either end of the stack.
/// </summary>
public bool CanPop => stack.Count > 0;
/// <summary>
/// Remove and return the value from the top of the stack.
/// </summary>
/// <returns>The value from the top of the stack.</returns>
public decimal PopTop()
{
if (stack.Count == 0)
{
throw new InvalidOperationException("Cannot pop from the top of an empty stack, invalid charstring parsed.");
}
var result = stack[stack.Count - 1];
stack.RemoveAt(stack.Count - 1);
return result;
}
/// <summary>
/// Remove and return the value from the bottom of the stack.
/// </summary>
/// <returns>The value from the bottom of the stack.</returns>
public decimal PopBottom()
{
if (stack.Count == 0)
{
throw new InvalidOperationException("Cannot pop from the bottom of an empty stack, invalid charstring parsed.");
}
var result = stack[0];
stack.RemoveAt(0);
return result;
}
/// <summary>
/// Adds the value to the top of the stack.
/// </summary>
/// <param name="value">The value to add.</param>
public void Push(decimal value)
{
stack.Add(value);
}
/// <summary>
/// Removes all values from the stack.
/// </summary>
public void Clear()
{
stack.Clear();
}
public override string ToString()
{
return string.Join(" ", stack.Select(x => x.ToString()));
}
}
}

View File

@@ -0,0 +1,37 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
using System;
using System.Diagnostics;
/// <summary>
/// Represents the deferred execution of a Type 2 CharString command.
/// </summary>
internal class LazyType2Command
{
private readonly Action<Type2BuildCharContext> runCommand;
public string Name { get; }
public LazyType2Command(string name, Action<Type2BuildCharContext> runCommand)
{
Name = name;
this.runCommand = runCommand ?? throw new ArgumentNullException(nameof(runCommand));
}
[DebuggerStepThrough]
public void Run(Type2BuildCharContext context)
{
runCommand(context);
}
public override string ToString()
{
return Name;
}
}
internal class Type2BuildCharContext
{
}
}

View File

@@ -0,0 +1,160 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
{
using System.Collections.Generic;
using Util;
/// <summary>
/// </summary>
/// <remarks>
/// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators.
/// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner
/// </remarks>
internal class Type2CharStringParser
{
public static void Parse(IReadOnlyList<IReadOnlyList<byte>> charStringBytes)
{
for (var i = 0; i < charStringBytes.Count; i++)
{
var charString = charStringBytes[i];
ParseSingle(charString);
}
}
private static IReadOnlyList<Union<decimal, LazyType2Command>> ParseSingle(IReadOnlyList<byte> bytes)
{
var instructions = new List<Union<decimal, LazyType2Command>>();
for (var i = 0; i < bytes.Count; i++)
{
var b = bytes[i];
if (b <= 31 && b != 28)
{
var command = GetCommand(b, bytes, ref i);
instructions.Add(Union<decimal, LazyType2Command>.Two(command));
}
else
{
var number = InterpretNumber(b, bytes, ref i);
instructions.Add(Union<decimal, LazyType2Command>.One(number));
}
}
return instructions;
}
/// <summary>
/// The Type 2 interpretation of a number with an initial byte value of 255 differs from how it is interpreted in the Type 1 format
/// and 28 has a special meaning.
/// </summary>
private static decimal InterpretNumber(byte b, IReadOnlyList<byte> bytes, ref int i)
{
if (b == 28)
{
return bytes[++i] << 8 | bytes[++i];
}
if (b >= 32 && b <= 246)
{
return b - 139;
}
if (b >= 247 && b <= 250)
{
var w = bytes[++i];
return ((b - 247) * 256) + w + 108;
}
if (b >= 251 && b <= 254)
{
var w = bytes[++i];
return -((b - 251) * 256) - w - 108;
}
/*
* If the charstring byte contains the value 255, the next four bytes indicate a two's complement signed number.
* The first of these the four bytes contains the highest order bits, the second byte contains the next higher order bits
* and the fourth byte contains the lowest order bits.
* This number is interpreted as a Fixed; that is, a signed number with 16 bits of fraction
*/
var lead = bytes[++i] << 8 | bytes[++i];
var fractionalPart = bytes[++i] << 8 | bytes[++i];
return lead + (fractionalPart / 65535m);
}
private static readonly IReadOnlyDictionary<int, LazyType2Command> SingleByteCommandStore = new Dictionary<int, LazyType2Command>
{
{ 1, new LazyType2Command("hstem", x => { })},
{ 3, new LazyType2Command("vstem", x => { })},
{ 4, new LazyType2Command("vmoveto", x => { })},
{ 5, new LazyType2Command("rlineto", x => { })},
{ 6, new LazyType2Command("hlineto", x => { })},
{ 7, new LazyType2Command("vlineto", x => { })},
{ 8, new LazyType2Command("rrcurveto", x => { })},
{ 10, new LazyType2Command("callsubr", x => { })},
{ 11, new LazyType2Command("return", x => { })},
{ 14, new LazyType2Command("endchar", x => { })},
{ 18, new LazyType2Command("hstemhm", x => { })},
{ 19, new LazyType2Command("hintmask", x => { })},
{ 20, new LazyType2Command("cntrmask", x => { })},
{ 21, new LazyType2Command("rmoveto", x => { })},
{ 22, new LazyType2Command("hmoveto", x => { })},
{ 23, new LazyType2Command("vstemhm", x => { })},
{ 24, new LazyType2Command("rcurveline", x => { })},
{ 25, new LazyType2Command("rlinecurve", x => { })},
{ 26, new LazyType2Command("vvcurveto", x => { })},
{ 27, new LazyType2Command("hhcurveto", x => { })},
{ 29, new LazyType2Command("callgsubr", x => { })},
{ 30, new LazyType2Command("vhcurveto", x => { })},
{ 31, new LazyType2Command("hvcurveto", x => { })}
};
private static readonly IReadOnlyDictionary<int, LazyType2Command> TwoByteCommandStore = new Dictionary<int, LazyType2Command>
{
{ 3, new LazyType2Command("and", x => { })},
{ 4, new LazyType2Command("or", x => { })},
{ 5, new LazyType2Command("not", x => { })},
{ 9, new LazyType2Command("abs", x => { })},
{ 10, new LazyType2Command("add", x => { })},
{ 11, new LazyType2Command("sub", x => { })},
{ 12, new LazyType2Command("div", x => { })},
{ 14, new LazyType2Command("neg", x => { })},
{ 15, new LazyType2Command("eq", x => { })},
{ 18, new LazyType2Command("drop", x => { })},
{ 20, new LazyType2Command("put", x => { })},
{ 21, new LazyType2Command("get", x => { })},
{ 22, new LazyType2Command("ifelse", x => { })},
{ 23, new LazyType2Command("random", x => { })},
{ 24, new LazyType2Command("mul", x => { })},
{ 26, new LazyType2Command("sqrt", x => { })},
{ 27, new LazyType2Command("dup", x => { })},
{ 28, new LazyType2Command("exch", x => { })},
{ 29, new LazyType2Command("index", x => { })},
{ 30, new LazyType2Command("roll", x => { })},
{ 34, new LazyType2Command("hflex", x => { })},
{ 35, new LazyType2Command("flex", x => { })},
{ 36, new LazyType2Command("hflex1", x => { })},
{ 37, new LazyType2Command("flex1", x => { })},
};
private static LazyType2Command GetCommand(byte b, IReadOnlyList<byte> bytes, ref int i)
{
if (b == 12)
{
var b2 = bytes[++i];
if (TwoByteCommandStore.TryGetValue(b2, out var commandTwoByte))
{
return commandTwoByte;
}
return new LazyType2Command($"unknown: {b} {b2}", x => {});
}
if (SingleByteCommandStore.TryGetValue(b, out var command))
{
return command;
}
return new LazyType2Command($"unknown: {b}", x => {});
}
}
}

View File

@@ -0,0 +1,103 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using System.Text;
/// <summary>
/// Provides access to the raw bytes of this Compact Font Format file with utility methods for reading data types from it.
/// </summary>
internal class CompactFontFormatData
{
private readonly byte[] dataBytes;
public int Position { get; private set; } = -1;
public CompactFontFormatData(byte[] dataBytes)
{
this.dataBytes = dataBytes;
}
public string ReadString(int length, Encoding encoding)
{
var bytes = new byte[length];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = ReadByte();
}
return encoding.GetString(bytes);
}
public byte ReadCard8()
{
return ReadByte();
}
public ushort ReadCard16()
{
return (ushort)(ReadByte() << 8 | ReadByte());
}
public byte ReadOffsize()
{
return ReadByte();
}
public int ReadOffset(int offsetSize)
{
var value = 0;
for (var i = 0; i < offsetSize; i++)
{
value = value << 8 | ReadByte();
}
return value;
}
public byte ReadByte()
{
Position++;
if (Position >= dataBytes.Length)
{
throw new IndexOutOfRangeException($"Cannot read byte at position {Position} of an array which is {dataBytes.Length} bytes long.");
}
return dataBytes[Position];
}
public byte Peek()
{
return dataBytes[Position + 1];
}
public bool CanRead()
{
return Position < dataBytes.Length - 1;
}
public void Seek(int offset)
{
Position = offset - 1;
}
public long ReadLong()
{
return (ReadCard16() << 16) | ReadCard16();
}
public byte[] ReadBytes(int length)
{
var result = new byte[length];
for (int i = 0; i < length; i++)
{
result[i] = ReadByte();
}
return result;
}
}
}

View File

@@ -0,0 +1,49 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
/// <summary>
/// The header table for the binary data of a Compact Font Format file.
/// </summary>
internal struct CompactFontFormatHeader
{
/// <summary>
/// The major version of this font format. Starting at 1.
/// </summary>
public byte MajorVersion { get; }
/// <summary>
/// The minor version of this font format. Starting at 0. Indicates extensions to the format which
/// are undetectable by readers which do not support them.
/// </summary>
public byte MinorVersion { get; }
/// <summary>
/// Indicates the size of this header in bytes so that future changes to the format may include extra data after the <see cref="OffsetSize"/> field.
/// </summary>
public byte SizeInBytes { get; }
/// <summary>
/// Specifies the size of all offsets relative to the start of the data in the font.
/// </summary>
public byte OffsetSize { get; }
/// <summary>
/// Creates a new <see cref="CompactFontFormatHeader"/>.
/// </summary>
/// <param name="majorVersion">The major version of this font format.</param>
/// <param name="minorVersion">The minor version of this font format.</param>
/// <param name="sizeInBytes">Indicates the size of this header in bytes so that future changes to the format may include extra data after the offsetSize field.</param>
/// <param name="offsetSize">Specifies the size of all offsets relative to the start of the data in the font.</param>
public CompactFontFormatHeader(byte majorVersion, byte minorVersion, byte sizeInBytes, byte offsetSize)
{
MajorVersion = majorVersion;
MinorVersion = minorVersion;
SizeInBytes = sizeInBytes;
OffsetSize = offsetSize;
}
public override string ToString()
{
return $"Major: {MajorVersion}, Minor: {MinorVersion}, Header Size: {SizeInBytes}, Offset: {OffsetSize}";
}
}
}

View File

@@ -1,5 +1,7 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using CharStrings;
using Dictionaries;
internal class CompactFontFormatIndividualFontParser
@@ -42,6 +44,17 @@
data.Seek(dictionary.CharStringsOffset);
var index = indexReader.ReadDictionaryData(data);
switch (dictionary.CharStringType)
{
case CompactFontFormatCharStringType.Type1:
throw new NotImplementedException();
case CompactFontFormatCharStringType.Type2:
Type2CharStringParser.Parse(index);
break;
default:
throw new ArgumentOutOfRangeException($"Unexpected CharString type in CFF font: {dictionary.CharStringType}.");
}
}
}
}

View File

@@ -1,7 +1,6 @@
namespace UglyToad.PdfPig.Fonts.CompactFontFormat
{
using System;
using System.Text;
using Util;
internal class CompactFontFormatParser
@@ -97,126 +96,4 @@
return result;
}
}
internal class CompactFontFormatData
{
private readonly byte[] dataBytes;
public int Position { get; private set; } = -1;
public CompactFontFormatData(byte[] dataBytes)
{
this.dataBytes = dataBytes;
}
public string ReadString(int length, Encoding encoding)
{
var bytes = new byte[length];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = ReadByte();
}
return encoding.GetString(bytes);
}
public byte ReadCard8()
{
return ReadByte();
}
public ushort ReadCard16()
{
return (ushort)(ReadByte() << 8 | ReadByte());
}
public byte ReadOffsize()
{
return ReadByte();
}
public int ReadOffset(int offsetSize)
{
var value = 0;
for (var i = 0; i < offsetSize; i++)
{
value = value << 8 | ReadByte();
}
return value;
}
public byte ReadByte()
{
Position++;
if (Position >= dataBytes.Length)
{
throw new IndexOutOfRangeException($"Cannot read byte at position {Position} of an array which is {dataBytes.Length} bytes long.");
}
return dataBytes[Position];
}
public byte Peek()
{
return dataBytes[Position + 1];
}
public bool CanRead()
{
return Position < dataBytes.Length - 1;
}
public void Seek(int offset)
{
Position = offset - 1;
}
public long ReadLong()
{
return (ReadCard16() << 16) | ReadCard16();
}
public byte[] ReadBytes(int length)
{
var result = new byte[length];
for (int i = 0; i < length; i++)
{
result[i] = ReadByte();
}
return result;
}
}
/// <summary>
/// The header table for the binary data of a CFF file.
/// </summary>
internal struct CompactFontFormatHeader
{
public byte MajorVersion { get; }
public byte MinorVersion { get; }
public byte SizeInBytes { get; }
public byte OffsetSize { get; }
public CompactFontFormatHeader(byte majorVersion, byte minorVersion, byte sizeInBytes, byte offsetSize)
{
MajorVersion = majorVersion;
MinorVersion = minorVersion;
SizeInBytes = sizeInBytes;
OffsetSize = offsetSize;
}
public override string ToString()
{
return $"Major: {MajorVersion}, Minor: {MinorVersion}, Header Size: {SizeInBytes}, Offset: {OffsetSize}";
}
}
}

View File

@@ -30,7 +30,7 @@
public decimal PaintType { get; set; }
public int CharstringType { get; set; } = 2;
public CompactFontFormatCharStringType CharStringType { get; set; } = CompactFontFormatCharStringType.Type2;
public TransformationMatrix FontMatrix { get; set; } = TransformationMatrix.FromValues(0.001m, 0m, 0.001m, 0, 0, 0);
@@ -62,5 +62,22 @@
public string BaseFontName { get; set; }
public decimal[] BaseFontBlend { get; set; }
}
/// <summary>
/// Defines the format of the CharString data contained within a Compact Font Format font.
/// </summary>
internal enum CompactFontFormatCharStringType
{
/// <summary>
/// The Type 1 CharString format as defined by the Adobe Type 1 Font Format.
/// </summary>
Type1 = 1,
/// <summary>
/// The Type 2 CharString format as defined by Adobe Technical Note #5177. This is the default type.
/// </summary>
Type2 = 2
}
}

View File

@@ -65,7 +65,7 @@
dictionary.PaintType = operands[0].Decimal;
break;
case 6:
dictionary.CharstringType = GetIntOrDefault(operands);
dictionary.CharStringType = (CompactFontFormatCharStringType)GetIntOrDefault(operands, 2);
break;
case 7:
{

View File

@@ -1,8 +1,7 @@
using System;
namespace UglyToad.PdfPig.Fonts.Type1.CharStrings.Commands
namespace UglyToad.PdfPig.Fonts.Type1.CharStrings.Commands
{
using System.Collections.Generic;
using System;
using System.Diagnostics;
/// <summary>
/// Represents the deferred execution of a Type 1 Build Char command.
@@ -19,6 +18,7 @@ namespace UglyToad.PdfPig.Fonts.Type1.CharStrings.Commands
this.runCommand = runCommand ?? throw new ArgumentNullException(nameof(runCommand));
}
[DebuggerStepThrough]
public void Run(Type1BuildCharContext context)
{
runCommand(context);
@@ -29,43 +29,4 @@ namespace UglyToad.PdfPig.Fonts.Type1.CharStrings.Commands
return Name;
}
}
internal class Type1Stack
{
private readonly List<decimal> stack = new List<decimal>();
public decimal PopTop()
{
if (stack.Count == 0)
{
throw new InvalidOperationException("Cannot pop from the top of an empty stack, invalid charstring parsed.");
}
var result = stack[stack.Count - 1];
stack.RemoveAt(stack.Count - 1);
return result;
}
public decimal PopBottom()
{
if (stack.Count == 0)
{
throw new InvalidOperationException("Cannot pop from the bottom of an empty stack, invalid charstring parsed.");
}
var result = stack[0];
stack.RemoveAt(0);
return result;
}
public void Push(decimal value)
{
stack.Add(value);
}
public void Clear()
{
stack.Clear();
}
}
}

View File

@@ -19,9 +19,9 @@
public PdfPoint CurrentPosition { get; set; }
public Type1Stack Stack { get; } = new Type1Stack();
public CharStringStack Stack { get; } = new CharStringStack();
public Type1Stack PostscriptStack { get; } = new Type1Stack();
public CharStringStack PostscriptStack { get; } = new CharStringStack();
public IReadOnlyList<PdfPoint> FlexPoints { get; }