| 
<?php
/*******************************************************************************
 * Utility to parse TTF font files                                              *
 *                                                                              *
 * Version: 1.0                                                                 *
 * Date:    2011-06-18                                                          *
 * Author:  Olivier PLATHEY                                                     *
 *******************************************************************************/
 
 class TTFParser
 {
 var $f;
 var $tables;
 var $unitsPerEm;
 var $xMin, $yMin, $xMax, $yMax;
 var $numberOfHMetrics;
 var $numGlyphs;
 var $widths;
 var $chars;
 var $postScriptName;
 var $Embeddable;
 var $Bold;
 var $typoAscender;
 var $typoDescender;
 var $capHeight;
 var $italicAngle;
 var $underlinePosition;
 var $underlineThickness;
 var $isFixedPitch;
 
 function Parse($file)
 {
 $this->f = fopen($file, 'rb');
 if(!$this->f)
 $this->Error('Can\'t open file: '.$file);
 
 $version = $this->Read(4);
 if($version=='OTTO')
 $this->Error('OpenType fonts based on PostScript outlines are not supported');
 if($version!="\x00\x01\x00\x00")
 $this->Error('Unrecognized file format');
 $numTables = $this->ReadUShort();
 $this->Skip(3*2); // searchRange, entrySelector, rangeShift
 $this->tables = array();
 for($i=0;$i<$numTables;$i++)
 {
 $tag = $this->Read(4);
 $this->Skip(4); // checkSum
 $offset = $this->ReadULong();
 $this->Skip(4); // length
 $this->tables[$tag] = $offset;
 }
 
 $this->ParseHead();
 $this->ParseHhea();
 $this->ParseMaxp();
 $this->ParseHmtx();
 $this->ParseCmap();
 $this->ParseName();
 $this->ParseOS2();
 $this->ParsePost();
 
 fclose($this->f);
 }
 
 function ParseHead()
 {
 $this->Seek('head');
 $this->Skip(3*4); // version, fontRevision, checkSumAdjustment
 $magicNumber = $this->ReadULong();
 if($magicNumber!=0x5F0F3CF5)
 $this->Error('Incorrect magic number');
 $this->Skip(2); // flags
 $this->unitsPerEm = $this->ReadUShort();
 $this->Skip(2*8); // created, modified
 $this->xMin = $this->ReadShort();
 $this->yMin = $this->ReadShort();
 $this->xMax = $this->ReadShort();
 $this->yMax = $this->ReadShort();
 }
 
 function ParseHhea()
 {
 $this->Seek('hhea');
 $this->Skip(4+15*2);
 $this->numberOfHMetrics = $this->ReadUShort();
 }
 
 function ParseMaxp()
 {
 $this->Seek('maxp');
 $this->Skip(4);
 $this->numGlyphs = $this->ReadUShort();
 }
 
 function ParseHmtx()
 {
 $this->Seek('hmtx');
 $this->widths = array();
 for($i=0;$i<$this->numberOfHMetrics;$i++)
 {
 $advanceWidth = $this->ReadUShort();
 $this->Skip(2); // lsb
 $this->widths[$i] = $advanceWidth;
 }
 if($this->numberOfHMetrics<$this->numGlyphs)
 {
 $lastWidth = $this->widths[$this->numberOfHMetrics-1];
 $this->widths = array_pad($this->widths, $this->numGlyphs, $lastWidth);
 }
 }
 
 function ParseCmap()
 {
 $this->Seek('cmap');
 $this->Skip(2); // version
 $numTables = $this->ReadUShort();
 $offset31 = 0;
 for($i=0;$i<$numTables;$i++)
 {
 $platformID = $this->ReadUShort();
 $encodingID = $this->ReadUShort();
 $offset = $this->ReadULong();
 if($platformID==3 && $encodingID==1)
 $offset31 = $offset;
 }
 if($offset31==0)
 $this->Error('No Unicode encoding found');
 
 $startCount = array();
 $endCount = array();
 $idDelta = array();
 $idRangeOffset = array();
 $this->chars = array();
 fseek($this->f, $this->tables['cmap']+$offset31, SEEK_SET);
 $format = $this->ReadUShort();
 if($format!=4)
 $this->Error('Unexpected subtable format: '.$format);
 $this->Skip(2*2); // length, language
 $segCount = $this->ReadUShort()/2;
 $this->Skip(3*2); // searchRange, entrySelector, rangeShift
 for($i=0;$i<$segCount;$i++)
 $endCount[$i] = $this->ReadUShort();
 $this->Skip(2); // reservedPad
 for($i=0;$i<$segCount;$i++)
 $startCount[$i] = $this->ReadUShort();
 for($i=0;$i<$segCount;$i++)
 $idDelta[$i] = $this->ReadShort();
 $offset = ftell($this->f);
 for($i=0;$i<$segCount;$i++)
 $idRangeOffset[$i] = $this->ReadUShort();
 
 for($i=0;$i<$segCount;$i++)
 {
 $c1 = $startCount[$i];
 $c2 = $endCount[$i];
 $d = $idDelta[$i];
 $ro = $idRangeOffset[$i];
 if($ro>0)
 fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
 for($c=$c1;$c<=$c2;$c++)
 {
 if($c==0xFFFF)
 break;
 if($ro>0)
 {
 $gid = $this->ReadUShort();
 if($gid>0)
 $gid += $d;
 }
 else
 $gid = $c+$d;
 if($gid>=65536)
 $gid -= 65536;
 if($gid>0)
 $this->chars[$c] = $gid;
 }
 }
 }
 
 function ParseName()
 {
 $this->Seek('name');
 $tableOffset = ftell($this->f);
 $this->postScriptName = '';
 $this->Skip(2); // format
 $count = $this->ReadUShort();
 $stringOffset = $this->ReadUShort();
 for($i=0;$i<$count;$i++)
 {
 $this->Skip(3*2); // platformID, encodingID, languageID
 $nameID = $this->ReadUShort();
 $length = $this->ReadUShort();
 $offset = $this->ReadUShort();
 if($nameID==6)
 {
 // PostScript name
 fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
 $s = $this->Read($length);
 $s = str_replace(chr(0), '', $s);
 $s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
 $this->postScriptName = $s;
 break;
 }
 }
 if($this->postScriptName=='')
 $this->Error('PostScript name not found');
 }
 
 function ParseOS2()
 {
 $this->Seek('OS/2');
 $version = $this->ReadUShort();
 $this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
 $fsType = $this->ReadUShort();
 $this->Embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
 $this->Skip(11*2+10+4*4+4);
 $fsSelection = $this->ReadUShort();
 $this->Bold = ($fsSelection & 32)!=0;
 $this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
 $this->typoAscender = $this->ReadShort();
 $this->typoDescender = $this->ReadShort();
 if($version>=2)
 {
 $this->Skip(3*2+2*4+2);
 $this->capHeight = $this->ReadShort();
 }
 else
 $this->capHeight = 0;
 }
 
 function ParsePost()
 {
 $this->Seek('post');
 $this->Skip(4); // version
 $this->italicAngle = $this->ReadShort();
 $this->Skip(2); // Skip decimal part
 $this->underlinePosition = $this->ReadShort();
 $this->underlineThickness = $this->ReadShort();
 $this->isFixedPitch = ($this->ReadULong()!=0);
 }
 
 function Error($msg)
 {
 if(PHP_SAPI=='cli')
 die("Error: $msg\n");
 else
 die("<b>Error</b>: $msg");
 }
 
 function Seek($tag)
 {
 if(!isset($this->tables[$tag]))
 $this->Error('Table not found: '.$tag);
 fseek($this->f, $this->tables[$tag], SEEK_SET);
 }
 
 function Skip($n)
 {
 fseek($this->f, $n, SEEK_CUR);
 }
 
 function Read($n)
 {
 return fread($this->f, $n);
 }
 
 function ReadUShort()
 {
 $a = unpack('nn', fread($this->f,2));
 return $a['n'];
 }
 
 function ReadShort()
 {
 $a = unpack('nn', fread($this->f,2));
 $v = $a['n'];
 if($v>=0x8000)
 $v -= 65536;
 return $v;
 }
 
 function ReadULong()
 {
 $a = unpack('NN', fread($this->f,4));
 return $a['N'];
 }
 }
 ?>
 
 |