create javascript to verify the output of the true type font parser. fix bugs

This commit is contained in:
Eliot Jones 2017-12-04 01:36:30 +00:00
parent 33c10a3ff7
commit 0ca417e21c
8 changed files with 600 additions and 15 deletions

View File

@ -0,0 +1,513 @@
<!DOCTYPE html>
<html>
<head>
<!-- Code taken from http://stevehanov.ca/blog/index.php?id=143 -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<style>
#dropTarget {
width: 100%;
min-width: 500px;
height: 1000px;
background: lavender;
border: 1px solid lightgray;
}
#details-container {
background: #333;
color: #ededed;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
font-size: 14px;
}
#details-container span {
margin: 5px;
display: block;
padding: 5px;
width: 160px;
}
</style>
</head>
<body id="dropTarget">
<div id="details-container"></div>
<div id="font-container"></div>
<script>
var dropTarget = document.getElementById("dropTarget");
dropTarget.ondragover = function (e) {
e.preventDefault();
};
dropTarget.ondrop = function (e) {
e.preventDefault();
if (!e.dataTransfer || !e.dataTransfer.files) {
alert("Your browser didn't include any files in the drop event");
return;
}
var reader = new FileReader();
reader.readAsArrayBuffer(e.dataTransfer.files[0]);
reader.onload = function (e) {
ShowTtfFile(reader.result);
};
};
function assert(condition) {
if (!condition) alert("False assert");
}
function BinaryReader(arrayBuffer) {
assert(arrayBuffer instanceof ArrayBuffer);
this.pos = 0;
this.data = new Uint8Array(arrayBuffer);
}
BinaryReader.prototype = {
seek: function (pos) {
assert(pos >= 0 && pos <= this.data.length);
var oldPos = this.pos;
this.pos = pos;
return oldPos;
},
tell: function () {
return this.pos;
},
getUint8: function () {
assert(this.pos < this.data.length);
return this.data[this.pos++];
},
getUint16: function () {
return ((this.getUint8() << 8) | this.getUint8()) >>> 0;
},
getUint32: function () {
return this.getInt32() >>> 0;
},
getInt16: function () {
var result = this.getUint16();
if (result & 0x8000) {
result -= (1 << 16);
}
return result;
},
getInt32: function () {
return ((this.getUint8() << 24) |
(this.getUint8() << 16) |
(this.getUint8() << 8) |
(this.getUint8()));
},
getFword: function () {
return this.getInt16();
},
get2Dot14: function () {
return this.getInt16() / (1 << 14);
},
getFixed: function () {
return this.getInt32() / (1 << 16);
},
getString: function (length) {
var result = "";
for (var i = 0; i < length; i++) {
result += String.fromCharCode(this.getUint8());
}
return result;
},
getDate: function () {
var macTime = this.getUint32() * 0x100000000 + this.getUint32();
var utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);
return new Date(utcTime);
}
};
function TrueTypeFont(arrayBuffer) {
this.file = new BinaryReader(arrayBuffer);
this.tables = this.readOffsetTables(this.file);
this.readHeadTable(this.file);
this.length = this.glyphCount();
}
TrueTypeFont.prototype = {
readOffsetTables: function (file) {
var tables = {};
this.scalarType = file.getUint32();
var numTables = file.getUint16();
this.searchRange = file.getUint16();
this.entrySelector = file.getUint16();
this.rangeShift = file.getUint16();
for (var i = 0; i < numTables; i++) {
var tag = file.getString(4);
tables[tag] = {
checksum: file.getUint32(),
offset: file.getUint32(),
length: file.getUint32()
};
if (tag !== 'head') {
assert(this.calculateTableChecksum(file, tables[tag].offset,
tables[tag].length) === tables[tag].checksum);
}
}
return tables;
},
calculateTableChecksum: function (file, offset, length) {
var old = file.seek(offset);
var sum = 0;
var nlongs = ((length + 3) / 4) | 0;
while (nlongs--) {
sum = (sum + file.getUint32() & 0xffffffff) >>> 0;
}
file.seek(old);
return sum;
},
glyphCount: function () {
assert("maxp" in this.tables);
var old = this.file.seek(this.tables["maxp"].offset + 4);
var count = this.file.getUint16();
this.file.seek(old);
return count;
},
readHeadTable: function (file) {
assert("head" in this.tables);
file.seek(this.tables["head"].offset);
this.version = file.getFixed();
this.fontRevision = file.getFixed();
this.checksumAdjustment = file.getUint32();
this.magicNumber = file.getUint32();
assert(this.magicNumber === 0x5f0f3cf5);
this.flags = file.getUint16();
this.unitsPerEm = file.getUint16();
this.created = file.getDate();
this.modified = file.getDate();
this.xMin = file.getFword();
this.yMin = file.getFword();
this.xMax = file.getFword();
this.yMax = file.getFword();
this.macStyle = file.getUint16();
this.lowestRecPPEM = file.getUint16();
this.fontDirectionHint = file.getInt16();
this.indexToLocFormat = file.getInt16();
this.glyphDataFormat = file.getInt16();
},
getGlyphOffset: function (index) {
assert("loca" in this.tables);
var table = this.tables["loca"];
var file = this.file;
var offset, old;
if (this.indexToLocFormat === 1) {
old = file.seek(table.offset + index * 4);
offset = file.getUint32();
} else {
old = file.seek(table.offset + index * 2);
offset = file.getUint16() * 2;
}
file.seek(old);
return offset + this.tables["glyf"].offset;
},
readGlyph: function (index) {
var offset = this.getGlyphOffset(index);
var file = this.file;
if (offset >= this.tables["glyf"].offset + this.tables["glyf"].length) {
return null;
}
assert(offset >= this.tables["glyf"].offset);
assert(offset < this.tables["glyf"].offset + this.tables["glyf"].length);
file.seek(offset);
var glyph = {
numberOfContours: file.getInt16(),
xMin: file.getFword(),
yMin: file.getFword(),
xMax: file.getFword(),
yMax: file.getFword()
};
assert(glyph.numberOfContours >= -1);
if (glyph.numberOfContours === -1) {
this.readCompoundGlyph(file, glyph);
} else {
this.readSimpleGlyph(file, glyph);
}
return glyph;
},
readSimpleGlyph: function (file, glyph) {
var ON_CURVE = 1,
X_IS_BYTE = 2,
Y_IS_BYTE = 4,
REPEAT = 8,
X_DELTA = 16,
Y_DELTA = 32;
glyph.type = "simple";
glyph.contourEnds = [];
var points = glyph.points = [];
for (var i = 0; i < glyph.numberOfContours; i++) {
glyph.contourEnds.push(file.getUint16());
}
// skip over intructions
file.seek(file.getUint16() + file.tell());
if (glyph.numberOfContours === 0) {
return;
}
var numPoints = Math.max.apply(null, glyph.contourEnds) + 1;
var flags = [];
for (i = 0; i < numPoints; i++) {
var flag = file.getUint8();
flags.push(flag);
points.push({
onCurve: (flag & ON_CURVE) > 0
});
if (flag & REPEAT) {
var repeatCount = file.getUint8();
assert(repeatCount > 0);
i += repeatCount;
while (repeatCount--) {
flags.push(flag);
points.push({
onCurve: (flag & ON_CURVE) > 0
});
}
}
}
function readCoords(name, byteFlag, deltaFlag, min, max) {
var value = 0;
for (var i = 0; i < numPoints; i++) {
var flag = flags[i];
if (flag & byteFlag) {
if (flag & deltaFlag) {
value += file.getUint8();
} else {
value -= file.getUint8();
}
} else if (~flag & deltaFlag) {
value += file.getInt16();
} else {
// value is unchanged.
}
points[i][name] = value;
}
}
readCoords("x", X_IS_BYTE, X_DELTA, glyph.xMin, glyph.xMax);
readCoords("y", Y_IS_BYTE, Y_DELTA, glyph.yMin, glyph.yMax);
},
readCompoundGlyph: function (file, glyph) {
var ARG_1_AND_2_ARE_WORDS = 1,
ARGS_ARE_XY_VALUES = 2,
ROUND_XY_TO_GRID = 4,
WE_HAVE_A_SCALE = 8,
// RESERVED = 16
MORE_COMPONENTS = 32,
WE_HAVE_AN_X_AND_Y_SCALE = 64,
WE_HAVE_A_TWO_BY_TWO = 128,
WE_HAVE_INSTRUCTIONS = 256,
USE_MY_METRICS = 512,
OVERLAP_COMPONENT = 1024;
glyph.type = "compound";
glyph.components = [];
var flags = MORE_COMPONENTS;
while (flags & MORE_COMPONENTS) {
var arg1, arg2;
flags = file.getUint16();
var component = {
glyphIndex: file.getUint16(),
matrix: {
a: 1, b: 0, c: 0, d: 1, e: 0, f: 0
}
};
if (flags & ARG_1_AND_2_ARE_WORDS) {
arg1 = file.getInt16();
arg2 = file.getInt16();
} else {
arg1 = file.getUint8();
arg2 = file.getUint8();
}
if (flags & ARGS_ARE_XY_VALUES) {
component.matrix.e = arg1;
component.matrix.f = arg2;
} else {
component.destPointIndex = arg1;
component.srcPointIndex = arg2;
}
if (flags & WE_HAVE_A_SCALE) {
component.matrix.a = file.get2Dot14();
component.matrix.d = component.matrix.a;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
component.matrix.a = file.get2Dot14();
component.matrix.d = file.get2Dot14();
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
component.matrix.a = file.get2Dot14();
component.matrix.b = file.get2Dot14();
component.matrix.c = file.get2Dot14();
component.matrix.d = file.get2Dot14();
}
glyph.components.push(component);
}
if (flags & WE_HAVE_INSTRUCTIONS) {
file.seek(file.getUint16() + file.tell());
}
},
drawGlyph: function (index, ctx) {
var glyph = this.readGlyph(index);
if (glyph === null || glyph.type !== "simple") {
return false;
}
var p = 0,
c = 0,
first = 1;
while (p < glyph.points.length) {
var point = glyph.points[p];
if (first === 1) {
ctx.moveTo(point.x, point.y);
first = 0;
} else {
ctx.lineTo(point.x, point.y);
}
if (p === glyph.contourEnds[c]) {
c += 1;
first = 1;
}
p += 1;
}
return true;
}
}
function CreateInformation(s, v, container) {
var span = document.createElement("span");
span.innerHTML = s + ": " + v;
container.appendChild(span);
}
function ShowTtfFile(arrayBuffer) {
var font = new TrueTypeFont(arrayBuffer);
var details = document.getElementById("details-container");
while (details.firstChild) details.removeChild(details.firstChild);
var content = "<table class='table table-striped'><thead><tr><td>Tag</td><td>Checksum</td><td>Offset</td><td>Length</td></tr></thead><tbody>";
for (var property in font.tables) {
if (font.tables.hasOwnProperty(property)) {
var tab = font.tables[property];
content += "<tr>";
content += "<td>" + property + "</td>";
content += "<td>" + tab.checksum + "</td>";
content += "<td>" + tab.offset + "</td>";
content += "<td>" + tab.length + "</td>";
content += "</tr>";
}
}
content += "</tbody></table>";
$("#details-container").append(content);
CreateInformation("Version", font.version, details);
CreateInformation("Revision", font.fontRevision, details);
CreateInformation("Checksum adjustment", font.checksumAdjustment, details);
CreateInformation("Magic Number", font.magicNumber, details);
CreateInformation("Flags", font.flags, details);
CreateInformation("Units Per Em", font.unitsPerEm, details);
CreateInformation("Created", font.created, details);
CreateInformation("Modified", font.modified, details);
CreateInformation("Min X", font.xMin, details);
CreateInformation("Min Y", font.yMin, details);
CreateInformation("Max X", font.xMax, details);
CreateInformation("Max Y", font.yMax, details);
CreateInformation("Mac Style", font.macStyle, details);
CreateInformation("Lowest recommended PPEM", font.lowestRecPPEM, details);
CreateInformation("Font Direction", font.fontDirectionHint, details);
CreateInformation("Index to loc format", font.indexToLocFormat, details);
CreateInformation("Glyph data format", font.glyphDataFormat, details);
var width = font.xMax - font.xMin;
var height = font.yMax - font.yMin;
var scale = 64 / font.unitsPerEm;
var container = document.getElementById("font-container");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
for (var i = 0; i < font.length; i++) {
var canvas = document.createElement("canvas");
canvas.style.border = "1px solid gray";
canvas.width = width * scale;
canvas.height = height * scale;
var ctx = canvas.getContext("2d");
ctx.scale(scale, -scale);
ctx.translate(-font.xMin, -font.yMin - height);
ctx.fillStyle = "#000000";
ctx.beginPath();
if (font.drawGlyph(i, ctx)) {
ctx.fill();
container.appendChild(canvas);
}
}
}
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@
using IO;
using Pdf.Fonts.TrueType;
using Pdf.Fonts.TrueType.Parser;
using Pdf.Fonts.TrueType.Tables;
using Xunit;
public class TrueTypeFontParserTests
@ -34,7 +35,47 @@
var input = new TrueTypeDataBytes(new ByteArrayInputBytes(bytes));
parser.Parse(input);
var font = parser.Parse(input);
Assert.Equal(1, font.Version);
Assert.Equal(1, font.HeaderTable.Version);
Assert.Equal(1, font.HeaderTable.Revision);
Assert.Equal(1142661421, font.HeaderTable.CheckSumAdjustment);
Assert.Equal(1594834165, font.HeaderTable.MagicNumber);
Assert.Equal(9, font.HeaderTable.Flags);
Assert.Equal(2048, font.HeaderTable.UnitsPerEm);
Assert.Equal(2008, font.HeaderTable.Created.Year);
Assert.Equal(10, font.HeaderTable.Created.Month);
Assert.Equal(13, font.HeaderTable.Created.Day);
Assert.Equal(12, font.HeaderTable.Created.Hour);
Assert.Equal(29, font.HeaderTable.Created.Minute);
Assert.Equal(34, font.HeaderTable.Created.Second);
Assert.Equal(2011, font.HeaderTable.Modified.Year);
Assert.Equal(12, font.HeaderTable.Modified.Month);
Assert.Equal(31, font.HeaderTable.Modified.Day);
Assert.Equal(5, font.HeaderTable.Modified.Hour);
Assert.Equal(13, font.HeaderTable.Modified.Minute);
Assert.Equal(10, font.HeaderTable.Modified.Second);
Assert.Equal(-980, font.HeaderTable.XMin);
Assert.Equal(-555, font.HeaderTable.YMin);
Assert.Equal(2396, font.HeaderTable.XMax);
Assert.Equal(2163, font.HeaderTable.YMax);
Assert.Equal(0, font.HeaderTable.MacStyle);
Assert.Equal(9, font.HeaderTable.LowestRecommendedPpem);
Assert.Equal(HeaderTable.FontDirection.StronglyLeftToRightWithNeutrals, font.HeaderTable.FontDirectionHint);
Assert.Equal(0, font.HeaderTable.IndexToLocFormat);
Assert.Equal(0, font.HeaderTable.GlyphDataFormat);
}
[Fact]

View File

@ -2,12 +2,13 @@
{
using Tables;
internal class HeaderTableParser : ITrueTypeTableParser
internal class HeaderTableParser
{
public string Tag => TrueTypeFontTable.Head;
public ITable Parse(TrueTypeDataBytes data, TrueTypeFontTable table)
public HeaderTable Parse(TrueTypeDataBytes data, TrueTypeFontTable table)
{
data.Seek(table.Offset - 1);
var version = data.Read32Fixed();
var fontRevision = data.Read32Fixed();
var checkSumAdjustment = data.ReadUnsignedInt();

View File

@ -0,0 +1,17 @@
namespace UglyToad.Pdf.Fonts.TrueType.Parser
{
using Tables;
internal class TrueTypeFont
{
public decimal Version { get; }
public HeaderTable HeaderTable { get; }
public TrueTypeFont(decimal version, HeaderTable headerTable)
{
Version = version;
HeaderTable = headerTable;
}
}
}

View File

@ -8,15 +8,11 @@
{
private const int TagLength = 4;
private static readonly IReadOnlyDictionary<string, ITrueTypeTableParser> parsers =
new Dictionary<string, ITrueTypeTableParser>
{
{TrueTypeFontTable.Head, new HeaderTableParser()}
};
public void Parse(TrueTypeDataBytes data)
private static readonly HeaderTableParser HeaderTableParser = new HeaderTableParser();
public TrueTypeFont Parse(TrueTypeDataBytes data)
{
var version = data.Read32Fixed();
var version = (decimal)data.Read32Fixed();
int numberOfTables = data.ReadUnsignedShort();
int searchRange = data.ReadUnsignedShort();
int entrySelector = data.ReadUnsignedShort();
@ -34,9 +30,9 @@
}
}
ParseTables(tables, data);
var result = ParseTables(version, tables, data);
return;
return result;
}
[CanBeNull]
@ -56,7 +52,7 @@
return new TrueTypeFontTable(tag, checksum, offset, length);
}
private static void ParseTables(IReadOnlyDictionary<string, TrueTypeFontTable> tables, TrueTypeDataBytes data)
private static TrueTypeFont ParseTables(decimal version, IReadOnlyDictionary<string, TrueTypeFontTable> tables, TrueTypeDataBytes data)
{
var isPostScript = tables.ContainsKey(TrueTypeFontTable.Cff);
@ -65,7 +61,9 @@
throw new InvalidOperationException($"The {TrueTypeFontTable.Head} table is required.");
}
var header = parsers[TrueTypeFontTable.Head].Parse(data, table);
var header = HeaderTableParser.Parse(data, table);
return new TrueTypeFont(version, header);
}
}
}

View File

@ -135,8 +135,15 @@
var date = new DateTime(1904, 1, 1, 0, 0, 0, 0, new GregorianCalendar());
var result = date.AddSeconds(secondsSince1904);
result = result.AddMonths(1);
result = result.AddDays(1);
return result;
}
public void Seek(long position)
{
inputBytes.Seek(position);
}
}
}

View File

@ -42,5 +42,11 @@
{
return CurrentOffset == bytes.Count - 1;
}
public void Seek(long position)
{
CurrentOffset = (int)position;
CurrentByte = bytes[CurrentOffset];
}
}
}

View File

@ -11,5 +11,7 @@
byte? Peek();
bool IsAtEnd();
void Seek(long position);
}
}