add horizontal header table parsing

This commit is contained in:
Eliot Jones
2017-12-04 21:52:10 +00:00
parent 207b0ebe7b
commit 236cdd286e
9 changed files with 271 additions and 152 deletions

View File

@@ -69,7 +69,7 @@
Assert.Equal(2396, font.HeaderTable.XMax);
Assert.Equal(2163, font.HeaderTable.YMax);
Assert.Equal(0, font.HeaderTable.MacStyle);
Assert.Equal(HeaderTable.HeaderMacStyle.None, font.HeaderTable.MacStyle);
Assert.Equal(9, font.HeaderTable.LowestRecommendedPpem);
Assert.Equal(HeaderTable.FontDirection.StronglyLeftToRightWithNeutrals, font.HeaderTable.FontDirectionHint);

View File

@@ -1,36 +0,0 @@
namespace UglyToad.Pdf.Fonts.TrueType.Parser
{
using Tables;
internal class HeaderTableParser
{
public string Tag => TrueTypeFontTable.Head;
public HeaderTable Parse(TrueTypeDataBytes data, TrueTypeFontTable table)
{
data.Seek(table.Offset - 1);
var version = data.Read32Fixed();
var fontRevision = data.Read32Fixed();
var checkSumAdjustment = data.ReadUnsignedInt();
var magicNumber = data.ReadUnsignedInt();
var flags = data.ReadUnsignedShort();
var unitsPerEm = data.ReadUnsignedShort();
var created = data.ReadInternationalDate();
var modified = data.ReadInternationalDate();
var xMin = data.ReadSignedShort();
var yMin = data.ReadSignedShort();
var xMax = data.ReadSignedShort();
var yMax = data.ReadSignedShort();
var macStyle = data.ReadUnsignedShort();
var lowestRecPpem = data.ReadUnsignedShort();
var fontDirectionHint = data.ReadSignedShort();
var indexToLocFormat = data.ReadSignedShort();
var glyphDataFormat = data.ReadSignedShort();
return new HeaderTable(table, (decimal)version, (decimal)fontRevision, checkSumAdjustment,
magicNumber, flags, unitsPerEm, created, modified,
xMin, yMin, xMax, yMax, macStyle, lowestRecPpem,
fontDirectionHint, indexToLocFormat, glyphDataFormat);
}
}
}

View File

@@ -1,11 +0,0 @@
namespace UglyToad.Pdf.Fonts.TrueType.Parser
{
using Tables;
internal interface ITrueTypeTableParser
{
string Tag { get; }
ITable Parse(TrueTypeDataBytes data, TrueTypeFontTable table);
}
}

View File

@@ -2,14 +2,11 @@
{
using System;
using System.Collections.Generic;
using Tables;
using Util.JetBrains.Annotations;
internal class TrueTypeFontParser
{
private const int TagLength = 4;
private static readonly HeaderTableParser HeaderTableParser = new HeaderTableParser();
public TrueTypeFont Parse(TrueTypeDataBytes data)
{
var version = (decimal)data.Read32Fixed();
@@ -18,15 +15,15 @@
int entrySelector = data.ReadUnsignedShort();
int rangeShift = data.ReadUnsignedShort();
var tables = new Dictionary<string, TrueTypeFontTable>();
var tables = new Dictionary<string, TrueTypeHeaderTable>();
for (var i = 0; i < numberOfTables; i++)
{
var table = ReadTable(data);
if (table != null)
if (table.HasValue)
{
tables[table.Tag] = table;
tables[table.Value.Tag] = table.Value;
}
}
@@ -36,32 +33,39 @@
}
[CanBeNull]
private static TrueTypeFontTable ReadTable(TrueTypeDataBytes data)
private static TrueTypeHeaderTable? ReadTable(TrueTypeDataBytes data)
{
var tag = data.ReadString(TagLength);
var tag = data.ReadTag();
var checksum = data.ReadUnsignedInt();
var offset = data.ReadUnsignedInt();
var length = data.ReadUnsignedInt();
// skip tables with zero length (except glyf)
if (length == 0 && !string.Equals(tag, TrueTypeFontTable.Glyf))
if (length == 0 && !string.Equals(tag, TrueTypeHeaderTable.Glyf))
{
return null;
}
return new TrueTypeFontTable(tag, checksum, offset, length);
return new TrueTypeHeaderTable(tag, checksum, offset, length);
}
private static TrueTypeFont ParseTables(decimal version, IReadOnlyDictionary<string, TrueTypeFontTable> tables, TrueTypeDataBytes data)
private static TrueTypeFont ParseTables(decimal version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tables, TrueTypeDataBytes data)
{
var isPostScript = tables.ContainsKey(TrueTypeFontTable.Cff);
var isPostScript = tables.ContainsKey(TrueTypeHeaderTable.Cff);
if (!tables.TryGetValue(TrueTypeFontTable.Head, out var table))
if (!tables.TryGetValue(TrueTypeHeaderTable.Head, out var table))
{
throw new InvalidOperationException($"The {TrueTypeFontTable.Head} table is required.");
throw new InvalidOperationException($"The {TrueTypeHeaderTable.Head} table is required.");
}
var header = HeaderTableParser.Parse(data, table);
var header = HeaderTable.Load(data, table);
if (!tables.TryGetValue(TrueTypeHeaderTable.Hhea, out var hHead))
{
throw new InvalidOperationException("The horizontal header table is required.");
}
var horizontalHeader = HorizontalHeaderTable.Load(data, hHead);
return new TrueTypeFont(version, header);
}

View File

@@ -7,19 +7,9 @@
/// </summary>
internal class HeaderTable : ITable
{
public string Tag => TrueTypeFontTable.Head;
public string Tag => TrueTypeHeaderTable.Head;
/// <summary>
/// Bold macStyle flag.
/// </summary>
public const int MacStyleBold = 1;
/// <summary>
/// Italic macStyle flag.
/// </summary>
public const int MacStyleItalic = 2;
public TrueTypeFontTable DirectoryTable { get; }
public TrueTypeHeaderTable DirectoryTable { get; }
public decimal Version { get; }
@@ -45,7 +35,7 @@
public short YMax { get; }
public int MacStyle { get; }
public HeaderMacStyle MacStyle { get; }
/// <summary>
/// Smallest readable size in pixels.
@@ -64,7 +54,7 @@
/// </summary>
public short GlyphDataFormat { get; }
public HeaderTable(TrueTypeFontTable directoryTable, decimal version, decimal revision, long checkSumAdjustment,
public HeaderTable(TrueTypeHeaderTable directoryTable, decimal version, decimal revision, long checkSumAdjustment,
long magicNumber, int flags, int unitsPerEm,
DateTime created, DateTime modified,
short xMin, short yMin,
@@ -75,7 +65,7 @@
short indexToLocFormat,
short glyphDataFormat)
{
DirectoryTable = directoryTable ?? throw new ArgumentNullException(nameof(directoryTable));
DirectoryTable = directoryTable;
Version = version;
Revision = revision;
CheckSumAdjustment = checkSumAdjustment;
@@ -88,13 +78,52 @@
YMin = yMin;
XMax = xMax;
YMax = yMax;
MacStyle = macStyle;
MacStyle = (HeaderMacStyle)macStyle;
LowestRecommendedPpem = lowestRecommendedPpem;
FontDirectionHint = (FontDirection)fontDirectionHint;
IndexToLocFormat = indexToLocFormat;
GlyphDataFormat = glyphDataFormat;
}
public static HeaderTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table)
{
data.Seek(table.Offset - 1);
var version = data.Read32Fixed();
var fontRevision = data.Read32Fixed();
var checkSumAdjustment = data.ReadUnsignedInt();
var magicNumber = data.ReadUnsignedInt();
if (magicNumber != 0x5F0F3CF5)
{
throw new InvalidOperationException("The magic number for this TrueType font was incorrect. Value was: " + magicNumber);
}
var flags = data.ReadUnsignedShort();
var unitsPerEm = data.ReadUnsignedShort();
if (unitsPerEm < 16 || unitsPerEm > 16384)
{
throw new InvalidOperationException($"The units per em for this TrueType font was incorrect, value should be between 16 and 16384 but found {unitsPerEm} istead.");
}
var created = data.ReadInternationalDate();
var modified = data.ReadInternationalDate();
var xMin = data.ReadSignedShort();
var yMin = data.ReadSignedShort();
var xMax = data.ReadSignedShort();
var yMax = data.ReadSignedShort();
var macStyle = data.ReadUnsignedShort();
var lowestRecPpem = data.ReadUnsignedShort();
var fontDirectionHint = data.ReadSignedShort();
var indexToLocFormat = data.ReadSignedShort();
var glyphDataFormat = data.ReadSignedShort();
return new HeaderTable(table, (decimal)version, (decimal)fontRevision, checkSumAdjustment,
magicNumber, flags, unitsPerEm, created, modified,
xMin, yMin, xMax, yMax, macStyle, lowestRecPpem,
fontDirectionHint, indexToLocFormat, glyphDataFormat);
}
public enum FontDirection
{
StronglyRightToLeftWithNeutrals = -2,
@@ -103,5 +132,18 @@
StronglyLeftToRight = 1,
StronglyLeftToRightWithNeutrals = 2
}
[Flags]
internal enum HeaderMacStyle : ushort
{
None = 0,
Bold = 1 << 0,
Italic = 1 << 1,
Underline = 1 << 2,
Outline = 1 << 3,
Shadow = 1 << 4,
Condensed = 1 << 5,
Extended = 1 << 6,
}
}
}

View File

@@ -0,0 +1,147 @@
namespace UglyToad.Pdf.Fonts.TrueType.Tables
{
using System;
internal class HorizontalHeaderTable
{
public string Tag => TrueTypeHeaderTable.Hhea;
public TrueTypeHeaderTable DirectoryTable { get; }
/// <summary>
/// Major version number of this table (1).
/// </summary>
public int MajorVersion { get; }
/// <summary>
/// Minor version number of this table (0).
/// </summary>
public int MinorVersion { get; }
/// <summary>
/// Distance from baseline to highest ascender.
/// </summary>
public short Ascender { get; }
/// <summary>
/// Distance from baseline to lower descender.
/// </summary>
public short Descender { get; }
/// <summary>
/// The typographic line gap.
/// </summary>
public short LineGap { get; }
/// <summary>
/// The maximum advance width value as given by the Horizontal Metrics table.
/// </summary>
public int AdvanceWidthMaximum { get; }
/// <summary>
/// The minimum left side bearing as given by the Horizontal Metrics table.
/// </summary>
public short MinimumLeftSideBearing { get; }
/// <summary>
/// The minimum right sidebearing.
/// </summary>
public short MinimumRightSideBearing { get; }
/// <summary>
/// The maximum X extent.
/// </summary>
public short XMaxExtent { get; }
/// <summary>
/// Used to calculate the slope of the cursor. 1 is vertical.
/// </summary>
public short CaretSlopeRise { get; }
/// <summary>
/// 0 is vertical.
/// </summary>
public short CaretSlopeRun { get; }
/// <summary>
/// The amount by which a slanted highlight on a glyph should be shifted to provide the best appearance. 0 for non-slanted fonts.
/// </summary>
public short CaretOffset { get; }
/// <summary>
/// 0 for the current format.
/// </summary>
public short MetricDataFormat { get; }
/// <summary>
/// Number of horizontal metrics in the Horizontal Metrics table.
/// </summary>
public int NumberOfHeaderMetrics { get; }
public HorizontalHeaderTable(TrueTypeHeaderTable directoryTable, int majorVersion, int minorVersion, short ascender, short descender, short lineGap, int advanceWidthMaximum, short minimumLeftSideBearing, short minimumRightSideBearing, short xMaxExtent, short caretSlopeRise, short caretSlopeRun, short caretOffset, short metricDataFormat, int numberOfHeaderMetrics)
{
DirectoryTable = directoryTable;
MajorVersion = majorVersion;
MinorVersion = minorVersion;
Ascender = ascender;
Descender = descender;
LineGap = lineGap;
AdvanceWidthMaximum = advanceWidthMaximum;
MinimumLeftSideBearing = minimumLeftSideBearing;
MinimumRightSideBearing = minimumRightSideBearing;
XMaxExtent = xMaxExtent;
CaretSlopeRise = caretSlopeRise;
CaretSlopeRun = caretSlopeRun;
CaretOffset = caretOffset;
MetricDataFormat = metricDataFormat;
NumberOfHeaderMetrics = numberOfHeaderMetrics;
}
public static HorizontalHeaderTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table)
{
data.Seek(table.Offset - 1);
var majorVersion = data.ReadUnsignedShort();
var minorVersion = data.ReadUnsignedShort();
var ascender = data.ReadSignedShort();
var descender = data.ReadSignedShort();
var lineGap = data.ReadSignedShort();
var advancedWidthMax = data.ReadUnsignedShort();
var minLeftSideBearing = data.ReadSignedShort();
var minRightSideBearing = data.ReadSignedShort();
var xMaxExtent = data.ReadSignedShort();
var caretSlopeRise = data.ReadSignedShort();
var caretSlopeRun = data.ReadSignedShort();
var caretOffset = data.ReadSignedShort();
// Reserved section
data.ReadSignedShort();
data.ReadSignedShort();
data.ReadSignedShort();
data.ReadSignedShort();
var metricDataFormat = data.ReadSignedShort();
if (metricDataFormat != 0)
{
throw new NotSupportedException("The metric data format for a horizontal header table should be 0.");
}
var numberOfHeaderMetrics = data.ReadSignedShort();
return new HorizontalHeaderTable(table, majorVersion, minorVersion, ascender,
descender, lineGap, advancedWidthMax,
minLeftSideBearing,
minRightSideBearing,
xMaxExtent,
caretSlopeRise,
caretSlopeRun,
caretOffset,
metricDataFormat,
numberOfHeaderMetrics);
}
}
}

View File

@@ -4,6 +4,6 @@
{
string Tag { get; }
TrueTypeFontTable DirectoryTable { get; }
TrueTypeHeaderTable DirectoryTable { get; }
}
}

View File

@@ -1,14 +1,13 @@
namespace UglyToad.Pdf.Fonts.TrueType
{
using System;
using System.Globalization;
using System.IO;
using System.Text;
using IO;
using Util;
internal class TrueTypeDataBytes
{
private readonly byte[] internalBuffer = new byte[16];
private readonly IInputBytes inputBytes;
public TrueTypeDataBytes(IInputBytes inputBytes)
@@ -25,106 +24,69 @@
public short ReadSignedShort()
{
int ch1 = Read();
int ch2 = Read();
if ((ch1 | ch2) < 0)
{
throw new EndOfStreamException();
}
ReadBuffered(internalBuffer, 2);
return (short)((ch1 << 8) + (ch2 << 0));
return unchecked((short)((internalBuffer[0] << 8) + (internalBuffer[1] << 0)));
}
public int ReadUnsignedShort()
{
int ch1 = Read();
int ch2 = Read();
if ((ch1 | ch2) < 0)
{
throw new EndOfStreamException();
}
ReadBuffered(internalBuffer, 2);
return (ch1 << 8) + (ch2 << 0);
return (internalBuffer[0] << 8) + (internalBuffer[1] << 0);
}
public int Read()
private void ReadBuffered(byte[] buffer, int length)
{
// We're no longer moving because we're at the end.
if (!inputBytes.MoveNext())
{
return -1;
}
int result = inputBytes.CurrentByte;
return (result + 256) % 256;
}
public byte[] Read(int numberOfBytes)
{
byte[] data = new byte[numberOfBytes];
int amountRead = 0;
while (amountRead < numberOfBytes)
var numberRead = 0;
while (numberRead < length)
{
if (!inputBytes.MoveNext())
{
throw new EndOfStreamException();
throw new EndOfStreamException($"Could not read a buffer of {length} bytes.");
}
data[amountRead] = inputBytes.CurrentByte;
amountRead++;
buffer[numberRead] = inputBytes.CurrentByte;
numberRead++;
}
return data;
}
public string ReadString(int length)
/// <summary>
/// Reads the 4 character tag from the TrueType file.
/// </summary>
public string ReadTag()
{
return ReadString(length, OtherEncodings.Iso88591);
return ReadString(4, Encoding.UTF8);
}
public string ReadString(int length, Encoding encoding)
public string ReadString(int bytesToRead, Encoding encoding)
{
byte[] buffer = Read(length);
var str = encoding.GetString(buffer);
return str;
byte[] data = new byte[bytesToRead];
ReadBuffered(data, bytesToRead);
return encoding.GetString(data, 0, data.Length);
}
public long ReadUnsignedInt()
{
long byte1 = Read();
long byte2 = Read();
long byte3 = Read();
long byte4 = Read();
if (byte4 < 0)
{
throw new EndOfStreamException();
}
return (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0);
ReadBuffered(internalBuffer, 4);
return (internalBuffer[0] << 24) + (internalBuffer[1] << 16) + (internalBuffer[2] << 8) + (internalBuffer[3] << 0);
}
public int ReadSignedInt()
{
int ch1 = Read();
int ch2 = Read();
int ch3 = Read();
int ch4 = Read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
{
throw new EndOfStreamException();
}
ReadBuffered(internalBuffer, 4);
return (ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0);
return (internalBuffer[0] << 24) + (internalBuffer[1] << 16) + (internalBuffer[2] << 8) + (internalBuffer[3] << 0);
}
public long ReadLong()
{
return (ReadSignedInt() << 32) + (ReadSignedInt() & 0xFFFFFFFFL);
ReadBuffered(internalBuffer, 8);
var result = FromBytes(internalBuffer, 0, 8);
return result;
}
public DateTime ReadInternationalDate()
@@ -132,7 +94,7 @@
// TODO: this returns the wrong value, investigate...
long secondsSince1904 = ReadLong();
var date = new DateTime(1904, 1, 1, 0, 0, 0, 0, new GregorianCalendar());
var date = new DateTime(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var result = date.AddSeconds(secondsSince1904);
result = result.AddMonths(1);
@@ -145,5 +107,16 @@
{
inputBytes.Seek(position);
}
private long FromBytes(byte[] buffer, int startIndex, int bytesToConvert)
{
long ret = 0;
for (int i = 0; i < bytesToConvert; i++)
{
ret = unchecked((ret << 8) | buffer[startIndex + i]);
}
return ret;
}
}
}

View File

@@ -3,7 +3,7 @@
/// <summary>
/// A table directory entry from the TrueType font file.
/// </summary>
internal class TrueTypeFontTable
internal struct TrueTypeHeaderTable
{
#region RequiredTableTags
/// <summary>
@@ -178,7 +178,7 @@
/// </summary>
public long Length { get; }
public TrueTypeFontTable(string tag, long checkSum, long offset, long length)
public TrueTypeHeaderTable(string tag, long checkSum, long offset, long length)
{
Tag = tag;
CheckSum = checkSum;