ttfparser.php 7.1 KB

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