Exploring the TrueType Font File Format in JavaScript

The JavaScript program below, when run, will present the user with a file upload button. If the user clicks this button and uploads a valid .ttf (TrueType font) file, the glyphs of that font will be displayed in outline.

This program displays the paths (or, as the TrueType specification calls them, “contours”) in outline, rather than as solid blocks of color, because the HTML5 canvas graphics context doesn’t really provide any easy way to fill the outline of the letter while still leaving the inner “counters” unfilled. For example, the letter “O” might render as just a giant black circle, with no empty space inside it. A simple way around this might be just to fill in the inner paths with the background color, or maybe to use fancy alpha compositing modes to “erase” the pixels inside the counters. But that raises the question of how to determine which paths are “inside” other paths. That might be possible with some sort of bounds checking, but that’s kind of a hack. The right thing to do is probably just to figure out the computational geometry, but that’s not a trivial task.

The program also doesn’t handle “composite” glyphs. Composite glyphs are composed of multiple simple glyphs. For example, if a language requires a glyph for “e” with an accent over it, this is often implemented as a composite glyph that references the simple glyphs for “e” and the accent mark.

I’d like to eventually update this code to allow the user to draw an arbitrary string in an arbitrary point size, using a TrueType font. Even though there’s certainly other JavaScript libraries out there that do it already, and for all I know it might be possible to do it with vanilla JavaScript.

Note that the image included with this post was made with an earlier version of this program, which incorrectly handled curved contours on glyphs. This has since been fixed.

UPDATE 2017/11/16 – I have updated this code to handle composite glyphs, to separate the display code from the font-parsing logic, and to pull more information from more header tables. It’s still pretty buggy, but for most fonts it can decode many more glyphs now.

I have also added an online version of this program at “https://thiscouldbebetter.neocities.org/truetypeviewer.html”, and created a Git repository for it at “https://github.com/thiscouldbebetter/TrueTypeViewer”.

TrueTypeFont


<html>
<body>

<div id="divUI">
	<p><b>TrueType Font Viewer</b></p>
	<p>Specify a valid .ttf file to view its contents:</p>
	<input id="inputFileToLoad" type="file" onchange="inputFileToLoad_Changed();" />
</div>

<script type="text/javascript">

// ui event handlers

function inputFileToLoad_Changed(event)
{
	var inputFileToLoad = document.getElementById("inputFileToLoad");
	var fileToLoad = inputFileToLoad.files[0];
	FileHelper.readBytesFromFile(fileToLoad, inputFileToLoad_Changed_FileLoaded)
}

function inputFileToLoad_Changed_FileLoaded(fileAsBytes)
{
	var fontName = "todo";
	var fontTrueType = new FontTrueType(fontName).fromBytes(fileAsBytes);
	var numberOfGlyphs = fontTrueType.maximumProfile.numberOfGlyphs;
	var fontHeightInPixels = 32;
	var glyphsPerRow = 20;
	var glyphRows = Math.ceil(numberOfGlyphs / glyphsPerRow);
	var displaySize = new Coords(glyphsPerRow, glyphRows).multiplyScalar
	(
		fontHeightInPixels
	);
	var display = new Display(displaySize);
	display.initialize();
	fontTrueType.drawToDisplay(display, fontHeightInPixels);
}

// classes

function ByteStreamBigEndian(bytes)
{
	this.bytes = bytes;  

	this.numberOfBytesTotal = this.bytes.length;
	this.byteIndexCurrent = 0;
}
{
	ByteStreamBigEndian.prototype.align16Bit = function()
	{
		while (this.byteIndexCurrent % 2 != 0)
		{
			this.readByte();
		}
	}

	ByteStreamBigEndian.prototype.align32Bit = function()
	{
		while (this.byteIndexCurrent % 4 != 0)
		{
			this.readByte();
		}
	}

	ByteStreamBigEndian.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent < this.numberOfBytesTotal);
	}

	ByteStreamBigEndian.prototype.peekBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.bytes.charCodeAt(this.byteIndexCurrent + b);
		}

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readByte = function()
	{
		var returnValue = this.bytes.charCodeAt(this.byteIndexCurrent);

		this.byteIndexCurrent++;

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readByteSigned = function()
	{
		var returnValue = this.readByte();
	
		var maxValue = 128; // hack
		if (returnValue >= maxValue)
		{
			returnValue -= maxValue + maxValue;
		}

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readFixedPoint16_16 = function()
	{
		var valueIntegral = this.readShort();
		var valueFractional = this.readShort();
		
		var valueAsString = "" + valueIntegral + "." + valueFractional;
	
		var returnValue = parseFloat(valueAsString);

		return returnValue;
	}


	ByteStreamBigEndian.prototype.readInt = function()
	{
		var returnValue =
		(
			((this.readByte() & 0xFF) << 24)
			| ((this.readByte() & 0xFF) << 16 )
			| ((this.readByte() & 0xFF) << 8 )
			| ((this.readByte() & 0xFF) )
		);

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readShort = function()
	{
		var returnValue =
		(
			((this.readByte() & 0xFF) << 8)
			| ((this.readByte() & 0xFF))
		);

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readShortSigned = function()
	{
		var returnValue =
		(
			((this.readByte() & 0xFF) << 8)
			| ((this.readByte() & 0xFF))
		);

		var maxValue = Math.pow(2, 15); // hack
		if (returnValue >= maxValue)
		{
			returnValue -= maxValue + maxValue;
		}

		return returnValue;
	}

	ByteStreamBigEndian.prototype.readString = function(numberOfBytesToRead)
	{
		var returnValue = "";

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			var charAsByte = this.readByte();
			returnValue += String.fromCharCode(charAsByte);
		}

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}
}

function Display(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;	
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.sizeInPixels.x;
		canvas.height = this.sizeInPixels.y;
		this.graphics = canvas.getContext("2d");
		this.graphics.strokeStyle = "Gray";
		document.body.appendChild(canvas);
	}

	// drawing

	Display.prototype.drawCurve = function(fromPos, curveControlPos, toPos, color)
	{
		if (color != null)
		{
			this.graphics.strokeStyle = color;
		}
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.quadraticCurveTo
		(
			curveControlPos.x, curveControlPos.y, toPos.x, toPos.y
		);
		this.graphics.stroke();
	}

	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		if (color != null)
		{
			this.graphics.strokeStyle = color;
		}
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}

	Display.prototype.drawRectangle = function(pos, size, color)
	{
		if (color != null)
		{
			this.graphics.strokeStyle = color;
		}
		this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);
	}
}

function FileHelper()
{}
{
	FileHelper.readBytesFromFile = function(file, callback)
	{
		var fileReader = new FileReader();
		fileReader.onloadend = function(fileLoadedEvent)
		{
			if (fileLoadedEvent.target.readyState == FileReader.DONE)
			{
				var bytesFromFile = fileLoadedEvent.target.result;
				callback(bytesFromFile);
			}
		}

		fileReader.readAsBinaryString(file);
	}
}

function FontTrueType(name)
{
	this.name = name;
}
{
	// drawable

	FontTrueType.prototype.drawToDisplay = function(display, fontHeightInPixels)
	{
		var glyphsPerRow = Math.floor(display.sizeInPixels.x / fontHeightInPixels);
		var numberOfGlyphs = this.glyphs.length;
		var glyphRows = Math.ceil(numberOfGlyphs / glyphsPerRow);

		var drawPos = new Coords(0, 0);		

		for (var g = 0; g < this.glyphs.length; g++)
		{
			var glyph = this.glyphs[g];

			drawPos.x = g % glyphsPerRow;
			drawPos.y = Math.floor(g / glyphsPerRow);
			drawPos.multiplyScalar(fontHeightInPixels);

			glyph.drawToDisplay(display, fontHeightInPixels, drawPos);
		}		
	}

	// file

	FontTrueType.prototype.fromBytes = function(bytesFromFile)
	{	
		var reader = new ByteStreamBigEndian(bytesFromFile);
		this.fromBytes_ReadTables(reader);
		return this;
	}

	FontTrueType.prototype.fromBytes_ReadTables = function(reader)
	{
		// offset table

		var sfntVersionAsBytes = reader.readInt();
		var numberOfTables = reader.readShort();
		var searchRange = reader.readShort(); // (max power of 2 <= numTables) * 16
		var entrySelector = reader.readShort(); // log2(max power of 2 <= numTables)
		var rangeShift = reader.readShort(); // numberOfTables * 16 - searchRange

		// table record entries

		var tableDefns = [];

		for (var t = 0; t < numberOfTables; t++)
		{
			var tableTypeTag = reader.readString(4);
			var checkSum = reader.readInt();
			var offsetInBytes = reader.readInt();
			var length = reader.readInt();
			
			var tableDefn = new FontTrueTypeTableDefn
			(
				tableTypeTag,
				checkSum,
				offsetInBytes,
				length
			);

			tableDefns.push(tableDefn);
			tableDefns[tableTypeTag] = tableDefn;
		}

		// Tables appear in alphabetical order in the file,
		// but because some depend on others,
		// they cannot be processed in that order.

		var tableNamesOrderedLogically = 
		[
			"head",
			"cmap",
			"maxp",
			"loca",
			"glyf"
		];

		for (var t = 0; t < tableNamesOrderedLogically.length; t++)
		{
			var tableName = tableNamesOrderedLogically[t];
			var tableDefn = tableDefns[tableName]; 
			reader.byteIndexCurrent = tableDefn.offsetInBytes;

			var tableTypeTag = tableDefn.tableTypeTag;			
			if (tableTypeTag == "cmap")
			{
				this.encodingTables = this.fromBytes_ReadTables_Cmap
				(
					reader, tableDefn.length
				);
			}
			else if (tableTypeTag == "glyf")
			{
				this.glyphs = this.fromBytes_ReadTables_Glyf
				(
					reader, tableDefn.length
				);
			}
			else if (tableTypeTag == "head")
			{
				this.headerTable = this.fromBytes_ReadTables_Head
				(
					reader, tableDefn.length
				);
			}
			else if (tableTypeTag == "loca")
			{
				this.indexToLocationTable = this.fromBytes_ReadTables_Loca
				(
					reader, tableDefn.length
				);
			}
			else if (tableTypeTag == "maxp")
			{
				this.maximumProfile = this.fromBytes_ReadTables_Maxp
				(
					reader, tableDefn.length
				);
			}
			else
			{
				console.log("Skipping table: " + tableTypeTag);
			}
		}
	}

	FontTrueType.prototype.fromBytes_ReadTables_Cmap = function(reader, length)
	{
		var readerByteOffsetOriginal = reader.byteIndexCurrent;

		var version = reader.readShort();

		var numberOfEncodingTables = reader.readShort();
		var encodingTables = [];

		for (var e = 0; e < numberOfEncodingTables; e++)
		{
			var platformID = reader.readShort(); // 3 = Microsoft
			var encodingID = reader.readShort(); // 1 = Unicode
			var offsetInBytes = reader.readInt(); // 32 bits, TrueType "long"

			var encodingTable = new FontTrueTypeCmapEncodingTable
			(
				platformID,
				encodingID,
				offsetInBytes
			);

			encodingTables.push(encodingTable);
		}
		
		for (var e = 0; e < numberOfEncodingTables; e++)
		{
			var encodingTable = encodingTables[e];

			reader.byteIndexCurrent = 
				readerByteOffsetOriginal 
				+ encodingTable.offsetInBytes;

			var formatCode = reader.readShort();
			if (formatCode == 0) 
			{
				// "Apple standard"
				var lengthInBytes = reader.readShort();
				var version = reader.readShort();
				var numberOfMappings = 256;
				encodingTable.charCodeToGlyphIndexLookup = reader.readBytes
				(
					numberOfMappings
				);
			}
			else if (formatCode == 2) 
			{
				// "high-byte mapping through table"
				// "useful for... Japanese, Chinese, and Korean"
				// "mixed 8/16-bit encoding"
				throw "Unsupported cmap format code."
			}
			else if (formatCode == 4)
			{
				// "Microsoft standard"
				// Not yet fully implemented.

				console.log("Unsupported cmap format code.")
				continue;

				var tableLengthInBytes = reader.readShort();
				var version = reader.readShort();
				var segmentCountTimes2 = reader.readShort();
				var segmentCount = segmentCount / 2;
				var searchRange = reader.readShort(); // "2 x (2**floor(log2(segCount)))"
				var entrySelector = reader.readShort(); // "log2(searchRange/2)"
				var rangeShift = reader.readShort(); // 2 x segCount - searchRange
				for (var s = 0; s < segmentCount; s++)
				{
					var segmentEndCharCode = reader.readShort();
				}
				var reservedPad = reader.readShort();
				for (var s = 0; s < segmentCount; s++)
				{
					var segmentStartCharCode = reader.readShort();
				}
				for (var s = 0; s < segmentCount; s++)
				{
					var idDeltaForCharCodesInSegment = reader.readShort();
				}
				for (var s = 0; s < segmentCount; s++)
				{
					var idRangeOffsetForSegment = reader.readShort();
				}

				while (true)
				{
					var glyphID = reader.readShort();
					break; // todo
				}
			}
			else if (formatCode == 6) 
			{
				// "Trimmed table mapping"
				throw "Unsupported cmap format code."
			}
			else
			{
				throw "Unrecognized cmap format code: " + formatCode
			}
		}

		reader.byteIndexCurrent = readerByteOffsetOriginal;

		return encodingTables;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Glyf = function(reader, length)
	{
		var glyphs = [];

		var byteIndexOfTable = reader.byteIndexCurrent;
		var bytesForContoursMinMax = 10;
		var glyphOffsetBase = byteIndexOfTable + bytesForContoursMinMax;

		while (reader.byteIndexCurrent < byteIndexOfTable + length)
		{
			// header
			var numberOfContours = reader.readShortSigned();
			var min = new Coords
			(
				reader.readShortSigned(),
				reader.readShortSigned()
			);
			var max = new Coords
			(
				reader.readShortSigned(),
				reader.readShortSigned()
			);
			var minAndMax = [min, max];

			var glyph;
			if (numberOfContours >= 0)
			{
				glyph = this.fromBytes_ReadTables_Glyf_Simple
				(
					reader, 
					numberOfContours, 
					minAndMax, 
					glyphOffsetBase
				);
			}
			else
			{
				glyph = this.fromBytes_ReadTables_Glyf_Composite
				(
					reader, 
					glyphOffsetBase
				);
			}

			reader.align16Bit();

			glyphs.push(glyph);
		}

		// hack
		// This is terrible, but then again, 
		// so is indexing glyphs by their byte offsets.
		for (var i = 0; i < glyphs.length; i++)
		{
			var glyph = glyphs[i];
			var glyphOffset = glyph.offsetInBytes;
			var glyphOffsetAsKey = "_" + glyphOffset;
			glyphs[glyphOffsetAsKey] = glyph;
		}

		return glyphs;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Glyf_Simple = function(reader, numberOfContours, minAndMax, byteIndexOfTable)
	{
		var offsetInBytes = reader.byteIndexCurrent - byteIndexOfTable;

		var endPointsOfContours = [];
		for (var c = 0; c < numberOfContours; c++)
		{
			var endPointOfContour = reader.readShort();
			endPointsOfContours.push(endPointOfContour);
		}

		var totalLengthOfInstructionsInBytes = reader.readShort();
		var instructionsAsBytes = reader.readBytes
		(
			totalLengthOfInstructionsInBytes
		);

		var numberOfPoints = 
			endPointsOfContours[endPointsOfContours.length - 1] 
			+ 1;

		var flagSets = [];
		var numberOfPointsSoFar = 0;
		while (numberOfPointsSoFar < numberOfPoints)
		{
			var flagsAsByte = reader.readByte();

			var flags = FontTrueTypeGlyphContourFlags.fromByte(flagsAsByte);

			flags.timesToRepeat  = (flags.timesToRepeat == true ? reader.readByte() : 0);	

			numberOfPointsSoFar += (1 + flags.timesToRepeat);
					
			flagSets.push(flags);
		}

		var coordinates = [];

		var xPrev = 0;
		for (var f = 0; f < flagSets.length; f++)
		{		
			var flags = flagSets[f];
			for (var r = 0; r <= flags.timesToRepeat; r++)
			{		
				var x;
				if (flags.xShortVector == true)
				{
					x = reader.readByte();
					var sign = (flags.xIsSame ? 1 : -1);
					x *= sign;
					x += xPrev;
				}
				else
				{	
					if (flags.xIsSame == true)
					{
						x = xPrev;
					}
					else
					{
						x = reader.readShortSigned();
						x += xPrev;
					}
				}

				var coordinate = new Coords(x, 0);
				coordinates.push(coordinate);
				xPrev = x;
			}
		}

		var yPrev = 0;
		var coordinateIndex = 0;
		for (var f = 0; f < flagSets.length; f++)
		{
			var flags = flagSets[f];
			for (var r = 0; r <= flags.timesToRepeat; r++)
			{
				var coordinate = coordinates[coordinateIndex];

				var y;
				if (flags.yShortVector == true)
				{
					y = reader.readByte();
					var sign = (flags.yIsSame ? 1 : -1);
					y *= sign;
					y += yPrev;
				}
				else
				{	
					if (flags.yIsSame == true)
					{
						y = yPrev;
					}
					else
					{
						y = reader.readShortSigned();
						y += yPrev;
					}
				}

				coordinate.y = y;
				yPrev = y;

				coordinateIndex++;
			}
		}

		reader.align16Bit();

		var glyph = new FontTrueTypeGlyph
		(
			minAndMax,
			endPointsOfContours,
			instructionsAsBytes,
			flagSets, 
			coordinates,
			offsetInBytes
		);

		return glyph;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Glyf_Composite = function(reader, byteIndexOfTable)
	{
		var offsetInBytes = reader.byteIndexCurrent - byteIndexOfTable;

		// "composite" glyph

		var flagSets = [];
		var flags = null;
		var childGlyphIndices = [];

		while (true)
		{
			var flagsAsShort = reader.readShort();
			flags = FontTrueTypeGlyphCompositeFlags.fromShort(flagsAsShort);
			flagSets.push(flags);

			var childGlyphIndex = reader.readShort();
			// childGlyphIndex -= 3; // hack
			childGlyphIndices.push(childGlyphIndex);

			var argument1 = (flags.areArgs1And2Words? reader.readShort() : reader.readByte());
			var argument2 = (flags.areArgs1And2Words? reader.readShort() : reader.readByte());
	
			if (flags.isThereASimpleScale == true)
			{
				var scaleFactor = reader.readShort();
				var scale = new Coords(scaleFactor, scaleFactor);
			}
			else if (flags.isXScaleDifferentFromYScale == true)
			{
				var scale = new Coords
				(
					reader.readShort(),
					reader.readShort()
				);
			}
			else if (flags.use2By2Transform == true)
			{
				// ???
				var scaleX = reader.readShort();
				var scale01 = reader.readShort();
				var scale02 = reader.readShort();
				var scaleY = reader.readShort();
			}

			if (flags.areThereMoreComponentGlyphs == false)
			{
				break;
			}
		}

		if (flags.areThereInstructions == true)
		{
			var numberOfInstructions = reader.readShort();
			var instructions = reader.readBytes(numberOfInstructions);
		}

		var glyphComposite = new FontTrueTypeGlyphComposite
		(
			this, childGlyphIndices, offsetInBytes
		);

		return glyphComposite;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Head = function(reader, length)
	{
		var tableVersion = reader.readFixedPoint16_16();
		var fontRevision = reader.readFixedPoint16_16();
		var checkSumAdjustment = reader.readInt(); // "To compute:  set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum."
		var magicNumber	= reader.readInt(); // 0x5F0F3CF5

		var flags = reader.readShort(); 
		// "Bit 0 - baseline for font at y=0;"
		// "Bit 1 - left sidebearing at x=0;"
		// "Bit 2 - instructions may depend on point size;"
		// "Bit 3 - force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear;"
		// "Bit 4 - instructions may alter advance width (the advance widths might not scale linearly);"
		// "Note: All other bits must be zero."

		var unitsPerEm = reader.readShort(); // "Valid range is from 16 to 16384"
		var timeCreated = reader.readBytes(8);
		var timeModified = reader.readBytes(8);
		var xMin = reader.readShortSigned(); // "For all glyph bounding boxes."
		var yMin = reader.readShortSigned();
		var xMax = reader.readShortSigned();
		var yMax = reader.readShortSigned();
		
		var macStyle = reader.readShort(); 
		// Bit 0 bold (if set to 1); Bit 1 italic (if set to 1)
		// Bits 2-15 reserved (set to 0).

		var lowestRecPPEM = reader.readShortSigned(); // "Smallest readable size in pixels."
		
		var fontDirectionHint = reader.readShortSigned();
		// 0   Fully mixed directional glyphs;
		// 1   Only strongly left to right;
		// 2   Like 1 but also contains neutrals ;
		//-1   Only strongly right to left;
		//-2   Like -1 but also contains neutrals.

		var returnValue = new FontTrueTypeHeaderTable
		(
			// todo
		);

		return returnValue;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Loca = function(reader, length)
	{
		// "Index to Location"

		var readerByteOffsetOriginal = reader.byteIndexCurrent;

		// "The version [short or long] is specified in the indexToLocFormat entry in the 'head' table."
		var isVersionShortRatherThanLong = false;

		var offsets = [];
		var valueRead = null;

		var numberOfGlyphs = this.maximumProfile.numberOfGlyphs;
		var numberOfGlyphsPlusOne = numberOfGlyphs + 1;

		if (isVersionShortRatherThanLong == true)
		{
			for (var i = 0; i < numberOfGlyphsPlusOne; i++)
			{
				valueRead = reader.readShort();
				var offset = valueRead * 2;
				offsets.push(offset);
			}
		}	
		else
		{
			for (var i = 0; i < numberOfGlyphsPlusOne; i++)
			{
				valueRead = reader.readInt();
				var offset = valueRead;
				offsets.push(offset);
			}
		}

		reader.byteIndexCurrent = readerByteOffsetOriginal;

		var returnValue = new FontTrueTypeLocationTable(offsets);

		return returnValue;
	}

	FontTrueType.prototype.fromBytes_ReadTables_Maxp = function(reader, length)
	{
		// "Maximum Profile"

		var readerByteOffsetOriginal = reader.byteIndexCurrent;

		var version = reader.readInt();
		var numberOfGlyphs = reader.readShort();
		var maxPointsPerFontTrueTypeGlyphSimple = reader.readShort();
		var maxContoursPerGlyphSimple = reader.readShort();
		var maxPointsPerGlyphComposite = reader.readShort();
		var maxContoursPerGlyphComposite = reader.readShort();

		// todo - Many more fields.

		reader.byteIndexCurrent = readerByteOffsetOriginal;

		var returnValue = new FontTrueTypeMaximumProfile(numberOfGlyphs);

		return returnValue;
	}
}

function FontTrueTypeCmapEncodingTable
(
	platformID,
	encodingID,
	offsetInBytes
)
{
	this.platformID = platformID;
	this.encodingID = encodingID; 
	this.offsetInBytes = offsetInBytes;

	// Unicode BMP for Windows : PlatformID = 3, EncodingID = 1 
}

function FontTrueTypeHeaderTable()
{
	// todo
}

function FontTrueTypeLocationTable(offsets)
{
	this.offsets = offsets;
}

function FontTrueTypeMaximumProfile(numberOfGlyphs)
{
	this.numberOfGlyphs = numberOfGlyphs;
}

function FontTrueTypeGlyph
(
	minAndMax,
	endPointsOfContours, 
	instructionsAsBytes, 
	flagSets, 
	coordinates,
	offsetInBytes
)
{
	this.minAndMax = minAndMax;
	this.endPointsOfContours = endPointsOfContours;
	this.instructionsAsBytes = instructionsAsBytes;
	this.flagSets = flagSets;
	this.coordinates = coordinates;
	this.offsetInBytes = offsetInBytes;
}
{
	// constants
	
	FontTrueTypeGlyph.PointsPerInch = 72;
	FontTrueTypeGlyph.DimensionInFUnits = 2048;

	// methods

	FontTrueTypeGlyph.prototype.drawToDisplay = function(display, fontHeightInPixels, drawOffset)
	{
		// todo
		var offsetForBaseLines = new Coords(.2, .2).multiplyScalar(fontHeightInPixels); 

		this.drawToDisplay_Background
		(
			display, fontHeightInPixels, offsetForBaseLines, drawOffset
		);

		var fUnitsPerPixel = FontTrueTypeGlyph.DimensionInFUnits / fontHeightInPixels;

		var contourPointSets = this.drawToDisplay_ContourPointSetsBuild
		(
			fUnitsPerPixel, offsetForBaseLines, fontHeightInPixels
		);

		var contours = this.drawToDisplay_ContoursBuild
		(
			contourPointSets
		);

		this.drawToDisplay_ContoursDraw(display, contours, drawOffset);
	}

	FontTrueTypeGlyph.prototype.drawToDisplay_Background = function
	(
		display, fontHeightInPixels, baseLineOffset, drawOffset
	)
	{
		display.drawRectangle
		(
			drawOffset, 
			new Coords(1, 1).multiplyScalar(fontHeightInPixels)
		);

		display.drawLine
		(
			new Coords(baseLineOffset.x, 0).add(drawOffset), 
			new Coords(baseLineOffset.x, fontHeightInPixels).add(drawOffset)
		);

		display.drawLine
		(
			new Coords(0, fontHeightInPixels - baseLineOffset.y).add(drawOffset),
			new Coords(fontHeightInPixels, fontHeightInPixels - baseLineOffset.y).add(drawOffset)
		)
	}

	FontTrueTypeGlyph.prototype.drawToDisplay_ContourPointSetsBuild = function
	(
		fUnitsPerPixel, offsetForBaseLines, fontHeightInPixels
	)
	{
		// Convert the flags and coordinates 
		// into sets of points on the contours of the glyph.

		var contourPointSets = [];

		var contourIndex = 0;
		var coordinateIndex = 0;
		var endPointOfContourCurrent = this.endPointsOfContours[contourIndex];

		var coordinateInFUnits = new Coords(0, 0);
		var coordinateInPixels = new Coords(0, 0);
		var coordinateInPixelsPrev = new Coords(0, 0);

		var numberOfContours = this.endPointsOfContours.length;
		var curveControlPoints = [];

		var contourPoints = [];

		for (var f = 0; f < this.flagSets.length; f++)
		{
			var flags = this.flagSets[f];
			for (var r = 0; r <= flags.timesToRepeat; r++)
			{
				var coordinateInFUnits = this.coordinates[coordinateIndex];

				coordinateInPixelsPrev.overwriteWith(coordinateInPixels);
				coordinateInPixels.overwriteWith
				(
					coordinateInFUnits
				).divideScalar
				(
					fUnitsPerPixel
				).add
				(
					offsetForBaseLines
				);

				coordinateInPixels.y = 
					fontHeightInPixels - coordinateInPixels.y;

				var contourPoint = new FontTrueTypeGlyphContourPoint
				(
					coordinateInPixels.clone(),
					flags.onCurve
				);

				contourPoints.push(contourPoint);

				if (coordinateIndex == endPointOfContourCurrent)
				{
					contourPointSets.push(contourPoints);
					contourPoints = [];
					contourIndex++;
					if (contourIndex < numberOfContours)
					{
						endPointOfContourCurrent = 
							this.endPointsOfContours[contourIndex];
					}
				}

				coordinateIndex++;
			}		
		}

		return contourPointSets;
	}

	FontTrueTypeGlyph.prototype.drawToDisplay_ContoursBuild = function(contourPointSets)
	{	
		// Convert sets of points on the contours of the glyph
		// into sets of line segments and/or curves, 
		// and build contours from those sets of segments and curves.

		var contours = [];

		for (var c = 0; c < contourPointSets.length; c++)
		{
			var contourPoints = contourPointSets[c];
			var contourSegments = [];

			for (var p = 0; p < contourPoints.length; p++)
			{
				var pNext = p + 1;
				if (pNext >= contourPoints.length)
				{
					pNext = 0;
				} 

				var contourPoint = contourPoints[p];
				var contourPointNext = contourPoints[pNext];

				if (contourPoint.isOnCurve == true)
				{
					if (contourPointNext.isOnCurve == true)
					{
						var segment = new FontTrueTypeGlyphContourSegment
						(
							contourPoint.position, null
						);

						contourSegments.push(segment);
					}
					else
					{
						var segment = new FontTrueTypeGlyphContourSegment
						(
							contourPoint.position, 
							contourPointNext.position
						);

						contourSegments.push(segment);
					}					
				}
				else // if (contourPoint.isOnCurve == false)
				{
					if (contourPointNext.isOnCurve == true)
					{
						// do nothing
					}
					else
					{
						var midpointBetweenContourPointAndNext = contourPoint.position.clone().add
						(
							contourPointNext.position
						).divideScalar(2);

						var segment = new FontTrueTypeGlyphContourSegment
						(
							midpointBetweenContourPointAndNext,
							contourPointNext.position
						);

						contourSegments.push(segment);
					}
				}
			}

			var contour = new FontTrueTypeGlyphContour(contourSegments);
			contours.push(contour);	
		}

		return contours;
	}

	FontTrueTypeGlyph.prototype.drawToDisplay_ContoursDraw = function(display, contours, drawOffset)
	{
		// Render the contours of the glyph.

		for (var c = 0; c < contours.length; c++)
		{
			var contour = contours[c];
			var contourSegments = contour.segments;
		
			for (var s = 0; s < contourSegments.length; s++)
			{
				var sNext = s + 1;
				if (sNext >= contourSegments.length)
				{
					sNext = 0;
				}

				var segment = contourSegments[s];
				var segmentNext = contourSegments[sNext];

				var startPoint = segment.startPoint.clone().add(drawOffset);
				var curveControlPoint = segment.curveControlPoint;
				var endPoint = segmentNext.startPoint.clone().add(drawOffset);

				if (curveControlPoint == null)
				{
					display.drawLine
					(
						startPoint,
						endPoint
					);
				}
				else
				{
					display.drawCurve
					(
						startPoint,
						curveControlPoint.clone().add(drawOffset), 
						endPoint, 
					);
				}
			} 
		}
	}
}

function FontTrueTypeGlyphComposite(font, childGlyphIndices, offsetInBytes)
{
	// todo
	this.font = font;
	this.childGlyphIndices = childGlyphIndices;
	this.offsetInBytes = offsetInBytes;
}
{
	FontTrueTypeGlyphComposite.prototype.childGlyphs = function()
	{
		var returnValues = [];

		var indexToLocationTable = this.font.indexToLocationTable;
		var glyphs = this.font.glyphs;

		for (var i = 0; i < this.childGlyphIndices.length; i++)
		{
			var childGlyphIndexNominal = this.childGlyphIndices[i];
			var childGlyphOffset = indexToLocationTable.offsets[childGlyphIndexNominal];
			var childGlyph = glyphs["_" + childGlyphOffset];
			returnValues.push(childGlyph);
		}
		return returnValues;
	}

	FontTrueTypeGlyphComposite.prototype.drawToDisplay = function(display, fontHeightInPixels, drawOffset)
	{
		var childGlyphs = this.childGlyphs();
		for (var i = 0; i < childGlyphs.length; i++)
		{
			var child = childGlyphs[i];
			child.drawToDisplay(display, fontHeightInPixels, drawOffset);	
		}
	}
}

function FontTrueTypeGlyphCompositeFlags
(
	areArgs1And2Words,
	areArgsXYValues,
	roundXYToGrid,
	isThereASimpleScale,
	reserved,
	areThereMoreComponentGlyphs,
	areXAndYScalesDifferent,
	use2By2Transform,
	areThereInstructions,
	useMyMetrics
)
{
	this.areArgs1And2Words = areArgs1And2Words;
	this.areArgsXYValues = areArgsXYValues;
	this.roundXYToGrid = roundXYToGrid;
	this.isThereASimpleScale = isThereASimpleScale;
	this.reserved = reserved;
	this.areThereMoreComponentGlyphs = areThereMoreComponentGlyphs;
	this.areXAndYScalesDifferent = areXAndYScalesDifferent;
	this.use2By2Transform = use2By2Transform;
	this.areThereInstructions = areThereInstructions,
	this.useMyMetrics = useMyMetrics;
}
{
	FontTrueTypeGlyphCompositeFlags.fromShort = function(flagsAsShort)
	{
		var returnValue = new FontTrueTypeGlyphCompositeFlags
		(
			((flagsAsShort & 1) > 0),
			((flagsAsShort >> 1 & 1) > 0),
			((flagsAsShort >> 2 & 1) > 0),
			((flagsAsShort >> 3 & 1) > 0),
			((flagsAsShort >> 4 & 1) > 0),
			((flagsAsShort >> 5 & 1) > 0),
			((flagsAsShort >> 6 & 1) > 0),		
			((flagsAsShort >> 7 & 1) > 0),
			((flagsAsShort >> 8 & 1) > 0),
			((flagsAsShort >> 9 & 1) > 0)
		);

		return returnValue;		
	}
}

function FontTrueTypeGlyphContour(segments)
{
	this.segments = segments;
}

function FontTrueTypeGlyphContourFlags
(
	onCurve, 
	xShortVector, 
	yShortVector, 
	timesToRepeat,
	xIsSame,
	yIsSame
)
{
	this.onCurve = onCurve;
	this.xShortVector = xShortVector;
	this.yShortVector = yShortVector;	
	this.timesToRepeat = timesToRepeat;
	this.xIsSame = xIsSame;
	this.yIsSame = yIsSame;

	// flag bits
	// 0 - on curve
	// 1 - xShortVector - coord is 1 instead of 2 bytes
	// 2 - yShortVector - coord is 1 instead of 2 bytes
	// 3 - repeat - if set, next byte is number of times to repeat
	// 4 - if xShortVector is 1, indicates sign of value (1 = positive, 0 = negative)
	// 4 - if xShortVector is 0, and this flag is 1, x coord is same as previous
	// 4 - if xShortVector is 0, and this flag is 0, x coord is a delta vector
	// 5 - same as 4, but for y instead of x
	// 6 - reserved
	// 7 - reserved	
}
{
	FontTrueTypeGlyphContourFlags.fromByte = function(flagsAsByte)
	{
		var returnValue = new FontTrueTypeGlyphContourFlags
		(
			((flagsAsByte & 1) > 0),
			((flagsAsByte >> 1 & 1) > 0),
			((flagsAsByte >> 2 & 1) > 0),
			((flagsAsByte >> 3 & 1) > 0),
			((flagsAsByte >> 4 & 1) > 0),
			((flagsAsByte >> 5 & 1) > 0)		
		);

		return returnValue;
	}
}

function FontTrueTypeGlyphContourPoint(position, isOnCurve)
{
	this.position = position;
	this.isOnCurve = isOnCurve;
}

function FontTrueTypeGlyphContourSegment(startPoint, curveControlPoint)
{
	this.startPoint = startPoint;
	this.curveControlPoint = curveControlPoint;
}

function FontTrueTypeTableDefn
(
	tableTypeTag,
	checkSum,
	offsetInBytes,
	length
)
{
	this.tableTypeTag = tableTypeTag;
	this.checkSum = checkSum;
	this.offsetInBytes = offsetInBytes;
	this.length = length;
}

</script>
</body>
</html>

This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

Leave a comment