| 1 | <?php
|
|---|
| 2 | /*******************************************************************************
|
|---|
| 3 | * Utility to parse TTF font files *
|
|---|
| 4 | * *
|
|---|
| 5 | * Version: 1.0 *
|
|---|
| 6 | * Date: 2011-06-18 *
|
|---|
| 7 | * Author: Olivier PLATHEY *
|
|---|
| 8 | *******************************************************************************/
|
|---|
| 9 |
|
|---|
| 10 | class TTFParser
|
|---|
| 11 | {
|
|---|
| 12 | var $f;
|
|---|
| 13 | var $tables;
|
|---|
| 14 | var $unitsPerEm;
|
|---|
| 15 | var $xMin, $yMin, $xMax, $yMax;
|
|---|
| 16 | var $numberOfHMetrics;
|
|---|
| 17 | var $numGlyphs;
|
|---|
| 18 | var $widths;
|
|---|
| 19 | var $chars;
|
|---|
| 20 | var $postScriptName;
|
|---|
| 21 | var $Embeddable;
|
|---|
| 22 | var $Bold;
|
|---|
| 23 | var $typoAscender;
|
|---|
| 24 | var $typoDescender;
|
|---|
| 25 | var $capHeight;
|
|---|
| 26 | var $italicAngle;
|
|---|
| 27 | var $underlinePosition;
|
|---|
| 28 | var $underlineThickness;
|
|---|
| 29 | var $isFixedPitch;
|
|---|
| 30 |
|
|---|
| 31 | function Parse($file)
|
|---|
| 32 | {
|
|---|
| 33 | $this->f = fopen($file, 'rb');
|
|---|
| 34 | if(!$this->f)
|
|---|
| 35 | $this->Error('Can\'t open file: '.$file);
|
|---|
| 36 |
|
|---|
| 37 | $version = $this->Read(4);
|
|---|
| 38 | if($version=='OTTO')
|
|---|
| 39 | $this->Error('OpenType fonts based on PostScript outlines are not supported');
|
|---|
| 40 | if($version!="\x00\x01\x00\x00")
|
|---|
| 41 | $this->Error('Unrecognized file format');
|
|---|
| 42 | $numTables = $this->ReadUShort();
|
|---|
| 43 | $this->Skip(3*2); // searchRange, entrySelector, rangeShift
|
|---|
| 44 | $this->tables = array();
|
|---|
| 45 | for($i=0;$i<$numTables;$i++)
|
|---|
| 46 | {
|
|---|
| 47 | $tag = $this->Read(4);
|
|---|
| 48 | $this->Skip(4); // checkSum
|
|---|
| 49 | $offset = $this->ReadULong();
|
|---|
| 50 | $this->Skip(4); // length
|
|---|
| 51 | $this->tables[$tag] = $offset;
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | $this->ParseHead();
|
|---|
| 55 | $this->ParseHhea();
|
|---|
| 56 | $this->ParseMaxp();
|
|---|
| 57 | $this->ParseHmtx();
|
|---|
| 58 | $this->ParseCmap();
|
|---|
| 59 | $this->ParseName();
|
|---|
| 60 | $this->ParseOS2();
|
|---|
| 61 | $this->ParsePost();
|
|---|
| 62 |
|
|---|
| 63 | fclose($this->f);
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | function ParseHead()
|
|---|
| 67 | {
|
|---|
| 68 | $this->Seek('head');
|
|---|
| 69 | $this->Skip(3*4); // version, fontRevision, checkSumAdjustment
|
|---|
| 70 | $magicNumber = $this->ReadULong();
|
|---|
| 71 | if($magicNumber!=0x5F0F3CF5)
|
|---|
| 72 | $this->Error('Incorrect magic number');
|
|---|
| 73 | $this->Skip(2); // flags
|
|---|
| 74 | $this->unitsPerEm = $this->ReadUShort();
|
|---|
| 75 | $this->Skip(2*8); // created, modified
|
|---|
| 76 | $this->xMin = $this->ReadShort();
|
|---|
| 77 | $this->yMin = $this->ReadShort();
|
|---|
| 78 | $this->xMax = $this->ReadShort();
|
|---|
| 79 | $this->yMax = $this->ReadShort();
|
|---|
| 80 | }
|
|---|
| 81 |
|
|---|
| 82 | function ParseHhea()
|
|---|
| 83 | {
|
|---|
| 84 | $this->Seek('hhea');
|
|---|
| 85 | $this->Skip(4+15*2);
|
|---|
| 86 | $this->numberOfHMetrics = $this->ReadUShort();
|
|---|
| 87 | }
|
|---|
| 88 |
|
|---|
| 89 | function ParseMaxp()
|
|---|
| 90 | {
|
|---|
| 91 | $this->Seek('maxp');
|
|---|
| 92 | $this->Skip(4);
|
|---|
| 93 | $this->numGlyphs = $this->ReadUShort();
|
|---|
| 94 | }
|
|---|
| 95 |
|
|---|
| 96 | function ParseHmtx()
|
|---|
| 97 | {
|
|---|
| 98 | $this->Seek('hmtx');
|
|---|
| 99 | $this->widths = array();
|
|---|
| 100 | for($i=0;$i<$this->numberOfHMetrics;$i++)
|
|---|
| 101 | {
|
|---|
| 102 | $advanceWidth = $this->ReadUShort();
|
|---|
| 103 | $this->Skip(2); // lsb
|
|---|
| 104 | $this->widths[$i] = $advanceWidth;
|
|---|
| 105 | }
|
|---|
| 106 | if($this->numberOfHMetrics<$this->numGlyphs)
|
|---|
| 107 | {
|
|---|
| 108 | $lastWidth = $this->widths[$this->numberOfHMetrics-1];
|
|---|
| 109 | $this->widths = array_pad($this->widths, $this->numGlyphs, $lastWidth);
|
|---|
| 110 | }
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | function ParseCmap()
|
|---|
| 114 | {
|
|---|
| 115 | $this->Seek('cmap');
|
|---|
| 116 | $this->Skip(2); // version
|
|---|
| 117 | $numTables = $this->ReadUShort();
|
|---|
| 118 | $offset31 = 0;
|
|---|
| 119 | for($i=0;$i<$numTables;$i++)
|
|---|
| 120 | {
|
|---|
| 121 | $platformID = $this->ReadUShort();
|
|---|
| 122 | $encodingID = $this->ReadUShort();
|
|---|
| 123 | $offset = $this->ReadULong();
|
|---|
| 124 | if($platformID==3 && $encodingID==1)
|
|---|
| 125 | $offset31 = $offset;
|
|---|
| 126 | }
|
|---|
| 127 | if($offset31==0)
|
|---|
| 128 | $this->Error('No Unicode encoding found');
|
|---|
| 129 |
|
|---|
| 130 | $startCount = array();
|
|---|
| 131 | $endCount = array();
|
|---|
| 132 | $idDelta = array();
|
|---|
| 133 | $idRangeOffset = array();
|
|---|
| 134 | $this->chars = array();
|
|---|
| 135 | fseek($this->f, $this->tables['cmap']+$offset31, SEEK_SET);
|
|---|
| 136 | $format = $this->ReadUShort();
|
|---|
| 137 | if($format!=4)
|
|---|
| 138 | $this->Error('Unexpected subtable format: '.$format);
|
|---|
| 139 | $this->Skip(2*2); // length, language
|
|---|
| 140 | $segCount = $this->ReadUShort()/2;
|
|---|
| 141 | $this->Skip(3*2); // searchRange, entrySelector, rangeShift
|
|---|
| 142 | for($i=0;$i<$segCount;$i++)
|
|---|
| 143 | $endCount[$i] = $this->ReadUShort();
|
|---|
| 144 | $this->Skip(2); // reservedPad
|
|---|
| 145 | for($i=0;$i<$segCount;$i++)
|
|---|
| 146 | $startCount[$i] = $this->ReadUShort();
|
|---|
| 147 | for($i=0;$i<$segCount;$i++)
|
|---|
| 148 | $idDelta[$i] = $this->ReadShort();
|
|---|
| 149 | $offset = ftell($this->f);
|
|---|
| 150 | for($i=0;$i<$segCount;$i++)
|
|---|
| 151 | $idRangeOffset[$i] = $this->ReadUShort();
|
|---|
| 152 |
|
|---|
| 153 | for($i=0;$i<$segCount;$i++)
|
|---|
| 154 | {
|
|---|
| 155 | $c1 = $startCount[$i];
|
|---|
| 156 | $c2 = $endCount[$i];
|
|---|
| 157 | $d = $idDelta[$i];
|
|---|
| 158 | $ro = $idRangeOffset[$i];
|
|---|
| 159 | if($ro>0)
|
|---|
| 160 | fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
|
|---|
| 161 | for($c=$c1;$c<=$c2;$c++)
|
|---|
| 162 | {
|
|---|
| 163 | if($c==0xFFFF)
|
|---|
| 164 | break;
|
|---|
| 165 | if($ro>0)
|
|---|
| 166 | {
|
|---|
| 167 | $gid = $this->ReadUShort();
|
|---|
| 168 | if($gid>0)
|
|---|
| 169 | $gid += $d;
|
|---|
| 170 | }
|
|---|
| 171 | else
|
|---|
| 172 | $gid = $c+$d;
|
|---|
| 173 | if($gid>=65536)
|
|---|
| 174 | $gid -= 65536;
|
|---|
| 175 | if($gid>0)
|
|---|
| 176 | $this->chars[$c] = $gid;
|
|---|
| 177 | }
|
|---|
| 178 | }
|
|---|
| 179 | }
|
|---|
| 180 |
|
|---|
| 181 | function ParseName()
|
|---|
| 182 | {
|
|---|
| 183 | $this->Seek('name');
|
|---|
| 184 | $tableOffset = ftell($this->f);
|
|---|
| 185 | $this->postScriptName = '';
|
|---|
| 186 | $this->Skip(2); // format
|
|---|
| 187 | $count = $this->ReadUShort();
|
|---|
| 188 | $stringOffset = $this->ReadUShort();
|
|---|
| 189 | for($i=0;$i<$count;$i++)
|
|---|
| 190 | {
|
|---|
| 191 | $this->Skip(3*2); // platformID, encodingID, languageID
|
|---|
| 192 | $nameID = $this->ReadUShort();
|
|---|
| 193 | $length = $this->ReadUShort();
|
|---|
| 194 | $offset = $this->ReadUShort();
|
|---|
| 195 | if($nameID==6)
|
|---|
| 196 | {
|
|---|
| 197 | // PostScript name
|
|---|
| 198 | fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
|
|---|
| 199 | $s = $this->Read($length);
|
|---|
| 200 | $s = str_replace(chr(0), '', $s);
|
|---|
| 201 | $s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
|
|---|
| 202 | $this->postScriptName = $s;
|
|---|
| 203 | break;
|
|---|
| 204 | }
|
|---|
| 205 | }
|
|---|
| 206 | if($this->postScriptName=='')
|
|---|
| 207 | $this->Error('PostScript name not found');
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | function ParseOS2()
|
|---|
| 211 | {
|
|---|
| 212 | $this->Seek('OS/2');
|
|---|
| 213 | $version = $this->ReadUShort();
|
|---|
| 214 | $this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
|
|---|
| 215 | $fsType = $this->ReadUShort();
|
|---|
| 216 | $this->Embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
|
|---|
| 217 | $this->Skip(11*2+10+4*4+4);
|
|---|
| 218 | $fsSelection = $this->ReadUShort();
|
|---|
| 219 | $this->Bold = ($fsSelection & 32)!=0;
|
|---|
| 220 | $this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
|
|---|
| 221 | $this->typoAscender = $this->ReadShort();
|
|---|
| 222 | $this->typoDescender = $this->ReadShort();
|
|---|
| 223 | if($version>=2)
|
|---|
| 224 | {
|
|---|
| 225 | $this->Skip(3*2+2*4+2);
|
|---|
| 226 | $this->capHeight = $this->ReadShort();
|
|---|
| 227 | }
|
|---|
| 228 | else
|
|---|
| 229 | $this->capHeight = 0;
|
|---|
| 230 | }
|
|---|
| 231 |
|
|---|
| 232 | function ParsePost()
|
|---|
| 233 | {
|
|---|
| 234 | $this->Seek('post');
|
|---|
| 235 | $this->Skip(4); // version
|
|---|
| 236 | $this->italicAngle = $this->ReadShort();
|
|---|
| 237 | $this->Skip(2); // Skip decimal part
|
|---|
| 238 | $this->underlinePosition = $this->ReadShort();
|
|---|
| 239 | $this->underlineThickness = $this->ReadShort();
|
|---|
| 240 | $this->isFixedPitch = ($this->ReadULong()!=0);
|
|---|
| 241 | }
|
|---|
| 242 |
|
|---|
| 243 | function Error($msg)
|
|---|
| 244 | {
|
|---|
| 245 | if(PHP_SAPI=='cli')
|
|---|
| 246 | die("Error: $msg\n");
|
|---|
| 247 | else
|
|---|
| 248 | die("<b>Error</b>: $msg");
|
|---|
| 249 | }
|
|---|
| 250 |
|
|---|
| 251 | function Seek($tag)
|
|---|
| 252 | {
|
|---|
| 253 | if(!isset($this->tables[$tag]))
|
|---|
| 254 | $this->Error('Table not found: '.$tag);
|
|---|
| 255 | fseek($this->f, $this->tables[$tag], SEEK_SET);
|
|---|
| 256 | }
|
|---|
| 257 |
|
|---|
| 258 | function Skip($n)
|
|---|
| 259 | {
|
|---|
| 260 | fseek($this->f, $n, SEEK_CUR);
|
|---|
| 261 | }
|
|---|
| 262 |
|
|---|
| 263 | function Read($n)
|
|---|
| 264 | {
|
|---|
| 265 | return fread($this->f, $n);
|
|---|
| 266 | }
|
|---|
| 267 |
|
|---|
| 268 | function ReadUShort()
|
|---|
| 269 | {
|
|---|
| 270 | $a = unpack('nn', fread($this->f,2));
|
|---|
| 271 | return $a['n'];
|
|---|
| 272 | }
|
|---|
| 273 |
|
|---|
| 274 | function ReadShort()
|
|---|
| 275 | {
|
|---|
| 276 | $a = unpack('nn', fread($this->f,2));
|
|---|
| 277 | $v = $a['n'];
|
|---|
| 278 | if($v>=0x8000)
|
|---|
| 279 | $v -= 65536;
|
|---|
| 280 | return $v;
|
|---|
| 281 | }
|
|---|
| 282 |
|
|---|
| 283 | function ReadULong()
|
|---|
| 284 | {
|
|---|
| 285 | $a = unpack('NN', fread($this->f,4));
|
|---|
| 286 | return $a['N'];
|
|---|
| 287 | }
|
|---|
| 288 | }
|
|---|
| 289 | ?>
|
|---|