1<?php
2/*******************************************************************************
3* Class to parse and subset TrueType fonts                                     *
4*                                                                              *
5* Version: 1.1                                                                 *
6* Date:    2015-11-29                                                          *
7* Author:  Olivier PLATHEY                                                     *
8*******************************************************************************/
9
10class TTFParser
11{
12	protected $f;
13	protected $tables;
14	protected $numberOfHMetrics;
15	protected $numGlyphs;
16	protected $glyphNames;
17	protected $indexToLocFormat;
18	protected $subsettedChars;
19	protected $subsettedGlyphs;
20	public $chars;
21	public $glyphs;
22	public $unitsPerEm;
23	public $xMin, $yMin, $xMax, $yMax;
24	public $postScriptName;
25	public $embeddable;
26	public $bold;
27	public $typoAscender;
28	public $typoDescender;
29	public $capHeight;
30	public $italicAngle;
31	public $underlinePosition;
32	public $underlineThickness;
33	public $isFixedPitch;
34
35	function __construct($file)
36	{
37		$this->f = fopen($file, 'rb');
38		if(!$this->f)
39			$this->Error('Can\'t open file: '.$file);
40	}
41
42	function __destruct()
43	{
44		if(is_resource($this->f))
45			fclose($this->f);
46	}
47
48	function Parse()
49	{
50		$this->ParseOffsetTable();
51		$this->ParseHead();
52		$this->ParseHhea();
53		$this->ParseMaxp();
54		$this->ParseHmtx();
55		$this->ParseLoca();
56		$this->ParseGlyf();
57		$this->ParseCmap();
58		$this->ParseName();
59		$this->ParseOS2();
60		$this->ParsePost();
61	}
62
63	function ParseOffsetTable()
64	{
65		$version = $this->Read(4);
66		if($version=='OTTO')
67			$this->Error('OpenType fonts based on PostScript outlines are not supported');
68		if($version!="\x00\x01\x00\x00")
69			$this->Error('Unrecognized file format');
70		$numTables = $this->ReadUShort();
71		$this->Skip(3*2); // searchRange, entrySelector, rangeShift
72		$this->tables = array();
73		for($i=0;$i<$numTables;$i++)
74		{
75			$tag = $this->Read(4);
76			$checkSum = $this->Read(4);
77			$offset = $this->ReadULong();
78			$length = $this->ReadULong(4);
79			$this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum);
80		}
81	}
82
83	function ParseHead()
84	{
85		$this->Seek('head');
86		$this->Skip(3*4); // version, fontRevision, checkSumAdjustment
87		$magicNumber = $this->ReadULong();
88		if($magicNumber!=0x5F0F3CF5)
89			$this->Error('Incorrect magic number');
90		$this->Skip(2); // flags
91		$this->unitsPerEm = $this->ReadUShort();
92		$this->Skip(2*8); // created, modified
93		$this->xMin = $this->ReadShort();
94		$this->yMin = $this->ReadShort();
95		$this->xMax = $this->ReadShort();
96		$this->yMax = $this->ReadShort();
97		$this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint
98		$this->indexToLocFormat = $this->ReadShort();
99	}
100
101	function ParseHhea()
102	{
103		$this->Seek('hhea');
104		$this->Skip(4+15*2);
105		$this->numberOfHMetrics = $this->ReadUShort();
106	}
107
108	function ParseMaxp()
109	{
110		$this->Seek('maxp');
111		$this->Skip(4);
112		$this->numGlyphs = $this->ReadUShort();
113	}
114
115	function ParseHmtx()
116	{
117		$this->Seek('hmtx');
118		$this->glyphs = array();
119		for($i=0;$i<$this->numberOfHMetrics;$i++)
120		{
121			$advanceWidth = $this->ReadUShort();
122			$lsb = $this->ReadShort();
123			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
124		}
125		for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++)
126		{
127			$lsb = $this->ReadShort();
128			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
129		}
130	}
131
132	function ParseLoca()
133	{
134		$this->Seek('loca');
135		$offsets = array();
136		if($this->indexToLocFormat==0)
137		{
138			// Short format
139			for($i=0;$i<=$this->numGlyphs;$i++)
140				$offsets[] = 2*$this->ReadUShort();
141		}
142		else
143		{
144			// Long format
145			for($i=0;$i<=$this->numGlyphs;$i++)
146				$offsets[] = $this->ReadULong();
147		}
148		for($i=0;$i<$this->numGlyphs;$i++)
149		{
150			$this->glyphs[$i]['offset'] = $offsets[$i];
151			$this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i];
152		}
153	}
154
155	function ParseGlyf()
156	{
157		$tableOffset = $this->tables['glyf']['offset'];
158		foreach($this->glyphs as &$glyph)
159		{
160			if($glyph['length']>0)
161			{
162				fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
163				if($this->ReadShort()<0)
164				{
165					// Composite glyph
166					$this->Skip(4*2); // xMin, yMin, xMax, yMax
167					$offset = 5*2;
168					$a = array();
169					do
170					{
171						$flags = $this->ReadUShort();
172						$index = $this->ReadUShort();
173						$a[$offset+2] = $index;
174						if($flags & 1) // ARG_1_AND_2_ARE_WORDS
175							$skip = 2*2;
176						else
177							$skip = 2;
178						if($flags & 8) // WE_HAVE_A_SCALE
179							$skip += 2;
180						elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE
181							$skip += 2*2;
182						elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO
183							$skip += 4*2;
184						$this->Skip($skip);
185						$offset += 2*2 + $skip;
186					}
187					while($flags & 32); // MORE_COMPONENTS
188					$glyph['components'] = $a;
189				}
190			}
191		}
192	}
193
194	function ParseCmap()
195	{
196		$this->Seek('cmap');
197		$this->Skip(2); // version
198		$numTables = $this->ReadUShort();
199		$offset31 = 0;
200		for($i=0;$i<$numTables;$i++)
201		{
202			$platformID = $this->ReadUShort();
203			$encodingID = $this->ReadUShort();
204			$offset = $this->ReadULong();
205			if($platformID==3 && $encodingID==1)
206				$offset31 = $offset;
207		}
208		if($offset31==0)
209			$this->Error('No Unicode encoding found');
210
211		$startCount = array();
212		$endCount = array();
213		$idDelta = array();
214		$idRangeOffset = array();
215		$this->chars = array();
216		fseek($this->f, $this->tables['cmap']['offset']+$offset31, SEEK_SET);
217		$format = $this->ReadUShort();
218		if($format!=4)
219			$this->Error('Unexpected subtable format: '.$format);
220		$this->Skip(2*2); // length, language
221		$segCount = $this->ReadUShort()/2;
222		$this->Skip(3*2); // searchRange, entrySelector, rangeShift
223		for($i=0;$i<$segCount;$i++)
224			$endCount[$i] = $this->ReadUShort();
225		$this->Skip(2); // reservedPad
226		for($i=0;$i<$segCount;$i++)
227			$startCount[$i] = $this->ReadUShort();
228		for($i=0;$i<$segCount;$i++)
229			$idDelta[$i] = $this->ReadShort();
230		$offset = ftell($this->f);
231		for($i=0;$i<$segCount;$i++)
232			$idRangeOffset[$i] = $this->ReadUShort();
233
234		for($i=0;$i<$segCount;$i++)
235		{
236			$c1 = $startCount[$i];
237			$c2 = $endCount[$i];
238			$d = $idDelta[$i];
239			$ro = $idRangeOffset[$i];
240			if($ro>0)
241				fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
242			for($c=$c1;$c<=$c2;$c++)
243			{
244				if($c==0xFFFF)
245					break;
246				if($ro>0)
247				{
248					$gid = $this->ReadUShort();
249					if($gid>0)
250						$gid += $d;
251				}
252				else
253					$gid = $c+$d;
254				if($gid>=65536)
255					$gid -= 65536;
256				if($gid>0)
257					$this->chars[$c] = $gid;
258			}
259		}
260	}
261
262	function ParseName()
263	{
264		$this->Seek('name');
265		$tableOffset = $this->tables['name']['offset'];
266		$this->postScriptName = '';
267		$this->Skip(2); // format
268		$count = $this->ReadUShort();
269		$stringOffset = $this->ReadUShort();
270		for($i=0;$i<$count;$i++)
271		{
272			$this->Skip(3*2); // platformID, encodingID, languageID
273			$nameID = $this->ReadUShort();
274			$length = $this->ReadUShort();
275			$offset = $this->ReadUShort();
276			if($nameID==6)
277			{
278				// PostScript name
279				fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
280				$s = $this->Read($length);
281				$s = str_replace(chr(0), '', $s);
282				$s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
283				$this->postScriptName = $s;
284				break;
285			}
286		}
287		if($this->postScriptName=='')
288			$this->Error('PostScript name not found');
289	}
290
291	function ParseOS2()
292	{
293		$this->Seek('OS/2');
294		$version = $this->ReadUShort();
295		$this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
296		$fsType = $this->ReadUShort();
297		$this->embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
298		$this->Skip(11*2+10+4*4+4);
299		$fsSelection = $this->ReadUShort();
300		$this->bold = ($fsSelection & 32)!=0;
301		$this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
302		$this->typoAscender = $this->ReadShort();
303		$this->typoDescender = $this->ReadShort();
304		if($version>=2)
305		{
306			$this->Skip(3*2+2*4+2);
307			$this->capHeight = $this->ReadShort();
308		}
309		else
310			$this->capHeight = 0;
311	}
312
313	function ParsePost()
314	{
315		$this->Seek('post');
316		$version = $this->ReadULong();
317		$this->italicAngle = $this->ReadShort();
318		$this->Skip(2); // Skip decimal part
319		$this->underlinePosition = $this->ReadShort();
320		$this->underlineThickness = $this->ReadShort();
321		$this->isFixedPitch = ($this->ReadULong()!=0);
322		if($version==0x20000)
323		{
324			// Extract glyph names
325			$this->Skip(4*4); // min/max usage
326			$this->Skip(2); // numberOfGlyphs
327			$glyphNameIndex = array();
328			$names = array();
329			$numNames = 0;
330			for($i=0;$i<$this->numGlyphs;$i++)
331			{
332				$index = $this->ReadUShort();
333				$glyphNameIndex[] = $index;
334				if($index>=258 && $index-257>$numNames)
335					$numNames = $index-257;
336			}
337			for($i=0;$i<$numNames;$i++)
338			{
339				$len = ord($this->Read(1));
340				$names[] = $this->Read($len);
341			}
342			foreach($glyphNameIndex as $i=>$index)
343			{
344				if($index>=258)
345					$this->glyphs[$i]['name'] = $names[$index-258];
346				else
347					$this->glyphs[$i]['name'] = $index;
348			}
349			$this->glyphNames = true;
350		}
351		else
352			$this->glyphNames = false;
353	}
354
355	function Subset($chars)
356	{
357/*		$chars = array_keys($this->chars);
358		$this->subsettedChars = $chars;
359		$this->subsettedGlyphs = array();
360		for($i=0;$i<$this->numGlyphs;$i++)
361		{
362			$this->subsettedGlyphs[] = $i;
363			$this->glyphs[$i]['ssid'] = $i;
364		}*/
365
366		$this->AddGlyph(0);
367		$this->subsettedChars = array();
368		foreach($chars as $char)
369		{
370			if(isset($this->chars[$char]))
371			{
372				$this->subsettedChars[] = $char;
373				$this->AddGlyph($this->chars[$char]);
374			}
375		}
376	}
377
378	function AddGlyph($id)
379	{
380		if(!isset($this->glyphs[$id]['ssid']))
381		{
382			$this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs);
383			$this->subsettedGlyphs[] = $id;
384			if(isset($this->glyphs[$id]['components']))
385			{
386				foreach($this->glyphs[$id]['components'] as $cid)
387					$this->AddGlyph($cid);
388			}
389		}
390	}
391
392	function Build()
393	{
394		$this->BuildCmap();
395		$this->BuildHhea();
396		$this->BuildHmtx();
397		$this->BuildLoca();
398		$this->BuildGlyf();
399		$this->BuildMaxp();
400		$this->BuildPost();
401		return $this->BuildFont();
402	}
403
404	function BuildCmap()
405	{
406		if(!isset($this->subsettedChars))
407			return;
408
409		// Divide charset in contiguous segments
410		$chars = $this->subsettedChars;
411		sort($chars);
412		$segments = array();
413		$segment = array($chars[0], $chars[0]);
414		for($i=1;$i<count($chars);$i++)
415		{
416			if($chars[$i]>$segment[1]+1)
417			{
418				$segments[] = $segment;
419				$segment = array($chars[$i], $chars[$i]);
420			}
421			else
422				$segment[1]++;
423		}
424		$segments[] = $segment;
425		$segments[] = array(0xFFFF, 0xFFFF);
426		$segCount = count($segments);
427
428		// Build a Format 4 subtable
429		$startCount = array();
430		$endCount = array();
431		$idDelta = array();
432		$idRangeOffset = array();
433		$glyphIdArray = '';
434		for($i=0;$i<$segCount;$i++)
435		{
436			list($start, $end) = $segments[$i];
437			$startCount[] = $start;
438			$endCount[] = $end;
439			if($start!=$end)
440			{
441				// Segment with multiple chars
442				$idDelta[] = 0;
443				$idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2;
444				for($c=$start;$c<=$end;$c++)
445				{
446					$ssid = $this->glyphs[$this->chars[$c]]['ssid'];
447					$glyphIdArray .= pack('n', $ssid);
448				}
449			}
450			else
451			{
452				// Segment with a single char
453				if($start<0xFFFF)
454					$ssid = $this->glyphs[$this->chars[$start]]['ssid'];
455				else
456					$ssid = 0;
457				$idDelta[] = $ssid - $start;
458				$idRangeOffset[] = 0;
459			}
460		}
461		$entrySelector = 0;
462		$n = $segCount;
463		while($n!=1)
464		{
465			$n = $n>>1;
466			$entrySelector++;
467		}
468		$searchRange = (1<<$entrySelector)*2;
469		$rangeShift = 2*$segCount - $searchRange;
470		$cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift);
471		foreach($endCount as $val)
472			$cmap .= pack('n', $val);
473		$cmap .= pack('n', 0); // reservedPad
474		foreach($startCount as $val)
475			$cmap .= pack('n', $val);
476		foreach($idDelta as $val)
477			$cmap .= pack('n', $val);
478		foreach($idRangeOffset as $val)
479			$cmap .= pack('n', $val);
480		$cmap .= $glyphIdArray;
481
482		$data = pack('nn', 0, 1); // version, numTables
483		$data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset
484		$data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language
485		$data .= $cmap;
486		$this->SetTable('cmap', $data);
487	}
488
489	function BuildHhea()
490	{
491		$this->LoadTable('hhea');
492		$numberOfHMetrics = count($this->subsettedGlyphs);
493		$data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2);
494		$this->SetTable('hhea', $data);
495	}
496
497	function BuildHmtx()
498	{
499		$data = '';
500		foreach($this->subsettedGlyphs as $id)
501		{
502			$glyph = $this->glyphs[$id];
503			$data .= pack('nn', $glyph['w'], $glyph['lsb']);
504		}
505		$this->SetTable('hmtx', $data);
506	}
507
508	function BuildLoca()
509	{
510		$data = '';
511		$offset = 0;
512		foreach($this->subsettedGlyphs as $id)
513		{
514			if($this->indexToLocFormat==0)
515				$data .= pack('n', $offset/2);
516			else
517				$data .= pack('N', $offset);
518			$offset += $this->glyphs[$id]['length'];
519		}
520		if($this->indexToLocFormat==0)
521			$data .= pack('n', $offset/2);
522		else
523			$data .= pack('N', $offset);
524		$this->SetTable('loca', $data);
525	}
526
527	function BuildGlyf()
528	{
529		$tableOffset = $this->tables['glyf']['offset'];
530		$data = '';
531		foreach($this->subsettedGlyphs as $id)
532		{
533			$glyph = $this->glyphs[$id];
534			fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
535			$glyph_data = $this->Read($glyph['length']);
536			if(isset($glyph['components']))
537			{
538				// Composite glyph
539				foreach($glyph['components'] as $offset=>$cid)
540				{
541					$ssid = $this->glyphs[$cid]['ssid'];
542					$glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2);
543				}
544			}
545			$data .= $glyph_data;
546		}
547		$this->SetTable('glyf', $data);
548	}
549
550	function BuildMaxp()
551	{
552		$this->LoadTable('maxp');
553		$numGlyphs = count($this->subsettedGlyphs);
554		$data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2);
555		$this->SetTable('maxp', $data);
556	}
557
558	function BuildPost()
559	{
560		$this->Seek('post');
561		if($this->glyphNames)
562		{
563			// Version 2.0
564			$numberOfGlyphs = count($this->subsettedGlyphs);
565			$numNames = 0;
566			$names = '';
567			$data = $this->Read(2*4+2*2+5*4);
568			$data .= pack('n', $numberOfGlyphs);
569			foreach($this->subsettedGlyphs as $id)
570			{
571				$name = $this->glyphs[$id]['name'];
572				if(is_string($name))
573				{
574					$data .= pack('n', 258+$numNames);
575					$names .= chr(strlen($name)).$name;
576					$numNames++;
577				}
578				else
579					$data .= pack('n', $name);
580			}
581			$data .= $names;
582		}
583		else
584		{
585			// Version 3.0
586			$this->Skip(4);
587			$data = "\x00\x03\x00\x00";
588			$data .= $this->Read(4+2*2+5*4);
589		}
590		$this->SetTable('post', $data);
591	}
592
593	function BuildFont()
594	{
595		$tags = array();
596		foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag)
597		{
598			if(isset($this->tables[$tag]))
599				$tags[] = $tag;
600		}
601		$numTables = count($tags);
602		$offset = 12 + 16*$numTables;
603		foreach($tags as $tag)
604		{
605			if(!isset($this->tables[$tag]['data']))
606				$this->LoadTable($tag);
607			$this->tables[$tag]['offset'] = $offset;
608			$offset += strlen($this->tables[$tag]['data']);
609		}
610//		$this->tables['head']['data'] = substr_replace($this->tables['head']['data'], "\x00\x00\x00\x00", 8, 4);
611
612		// Build offset table
613		$entrySelector = 0;
614		$n = $numTables;
615		while($n!=1)
616		{
617			$n = $n>>1;
618			$entrySelector++;
619		}
620		$searchRange = 16*(1<<$entrySelector);
621		$rangeShift = 16*$numTables - $searchRange;
622		$offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift);
623		foreach($tags as $tag)
624		{
625			$table = $this->tables[$tag];
626			$offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']);
627		}
628
629		// Compute checkSumAdjustment (0xB1B0AFBA - font checkSum)
630		$s = $this->CheckSum($offsetTable);
631		foreach($tags as $tag)
632			$s .= $this->tables[$tag]['checkSum'];
633		$a = unpack('n2', $this->CheckSum($s));
634		$high = 0xB1B0 + ($a[1]^0xFFFF);
635		$low = 0xAFBA + ($a[2]^0xFFFF) + 1;
636		$checkSumAdjustment = pack('nn', $high+($low>>16), $low);
637		$this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4);
638
639		$font = $offsetTable;
640		foreach($tags as $tag)
641			$font .= $this->tables[$tag]['data'];
642
643		return $font;
644	}
645
646	function LoadTable($tag)
647	{
648		$this->Seek($tag);
649		$length = $this->tables[$tag]['length'];
650		$n = $length % 4;
651		if($n>0)
652			$length += 4 - $n;
653		$this->tables[$tag]['data'] = $this->Read($length);
654	}
655
656	function SetTable($tag, $data)
657	{
658		$length = strlen($data);
659		$n = $length % 4;
660		if($n>0)
661			$data = str_pad($data, $length+4-$n, "\x00");
662		$this->tables[$tag]['data'] = $data;
663		$this->tables[$tag]['length'] = $length;
664		$this->tables[$tag]['checkSum'] = $this->CheckSum($data);
665	}
666
667	function Seek($tag)
668	{
669		if(!isset($this->tables[$tag]))
670			$this->Error('Table not found: '.$tag);
671		fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET);
672	}
673
674	function Skip($n)
675	{
676		fseek($this->f, $n, SEEK_CUR);
677	}
678
679	function Read($n)
680	{
681		return $n>0 ? fread($this->f, $n) : '';
682	}
683
684	function ReadUShort()
685	{
686		$a = unpack('nn', fread($this->f,2));
687		return $a['n'];
688	}
689
690	function ReadShort()
691	{
692		$a = unpack('nn', fread($this->f,2));
693		$v = $a['n'];
694		if($v>=0x8000)
695			$v -= 65536;
696		return $v;
697	}
698
699	function ReadULong()
700	{
701		$a = unpack('NN', fread($this->f,4));
702		return $a['N'];
703	}
704
705	function CheckSum($s)
706	{
707		$n = strlen($s);
708		$high = 0;
709		$low = 0;
710		for($i=0;$i<$n;$i+=4)
711		{
712			$high += (ord($s[$i])<<8) + ord($s[$i+1]);
713			$low += (ord($s[$i+2])<<8) + ord($s[$i+3]);
714		}
715		return pack('nn', $high+($low>>16), $low);
716	}
717
718	function Error($msg)
719	{
720		throw new Exception($msg);
721	}
722}
723?>
724