28152c1de336c74a09d58810b942b1f425b79268
[pear] / Fpdf / font / unifont / ttfonts.php
1 <?php\r
2 \r
3 \r
4 /*******************************************************************************\r
5 * TTFontFile class                                                             *\r
6 *                                                                              *\r
7 * This class is based on The ReportLab Open Source PDF library                 *\r
8 * written in Python - http://www.reportlab.com/software/opensource/            *\r
9 * together with ideas from the OpenOffice source code and others.              *\r
10 *                                                                              *\r
11 * Version:  1.02                                                               *\r
12 * Date:     2010-06-07                                                         *\r
13 * Author:   Ian Back <ianb@bpm1.com>                                           *\r
14 * License:  LGPL                                                               *\r
15 * Copyright (c) Ian Back, 2010                                                 *\r
16 * This header must be retained in any redistribution or                        *\r
17 * modification of the file.                                                    *\r
18 *                                                                              *\r
19 *******************************************************************************/\r
20 // TrueType Font Glyph operators\r
21 define("GF_WORDS",(1 << 0));\r
22 define("GF_SCALE",(1 << 3));\r
23 define("GF_MORE",(1 << 5));\r
24 define("GF_XYSCALE",(1 << 6));\r
25 define("GF_TWOBYTWO",(1 << 7));\r
26 \r
27 \r
28 \r
29 class TTFontFile {\r
30 \r
31 var $_pos;\r
32 var $numTables;\r
33 var $searchRange;\r
34 var $entrySelector;\r
35 var $rangeShift;\r
36 var $tables;\r
37 var $otables;\r
38 var $filename;\r
39 var $fh;\r
40 var $hmetrics;\r
41 var $glyphPos;\r
42 var $charToGlyph;\r
43 var $ascent;\r
44 var $descent;\r
45 var $name;\r
46 var $familyName;\r
47 var $styleName;\r
48 var $fullName;\r
49 var $uniqueFontID;\r
50 var $unitsPerEm;\r
51 var $bbox;\r
52 var $capHeight;\r
53 var $stemV;\r
54 var $italicAngle;\r
55 var $flags;\r
56 var $underlinePosition;\r
57 var $underlineThickness;\r
58 var $charWidths;\r
59 var $defaultWidth;\r
60 var $maxStrLenRead;\r
61 \r
62         function __construct() {\r
63                 $this->maxStrLenRead = 200000;  // Maximum size of glyf table to read in as string (otherwise reads each glyph from file)\r
64         }\r
65 \r
66 \r
67         function getMetrics($file, $presubset=0) {\r
68                 $this->filename = $file;\r
69                 $this->fh = fopen($file,'rb') or die('Can\'t open file ' . $file);\r
70                 $this->_pos = 0;\r
71                 $this->charWidths = array();\r
72                 $this->hmetrics = array();\r
73                 $this->glyphPos = array();\r
74                 $this->charToGlyph = array();\r
75                 $this->tables = array();\r
76                 $this->otables = array();\r
77                 $this->ascent = 0;\r
78                 $this->descent = 0;\r
79                 if ($presubset) { $this->skip(4); }\r
80                 else { $this->readHeader(); }\r
81                 $this->readTableDirectory($presubset);\r
82                 $this->extractInfo($presubset); \r
83                 fclose($this->fh);\r
84         }\r
85 \r
86         function readHeader() {\r
87                 // read the sfnt header at the current position\r
88                 $this->version = $version = $this->read_ulong();\r
89                 if ($version==0x4F54544F) \r
90                         die("Postscript outlines are not supported");\r
91                 if ($version==0x74746366) \r
92                         die("TTC Font collections are not supported");\r
93                 if (!in_array($version, array(0x00010000,0x74727565)))\r
94                         die("Not a TrueType font: version=".$version);\r
95                 return true;\r
96         }\r
97 \r
98         function readTableDirectory($presubset=1) {\r
99                 $this->numTables = $this->read_ushort();\r
100             $this->searchRange = $this->read_ushort();\r
101             $this->entrySelector = $this->read_ushort();\r
102             $this->rangeShift = $this->read_ushort();\r
103             $this->tables = array();    \r
104             for ($i=0;$i<$this->numTables;$i++) {       // 1.02\r
105                 $record = array();\r
106                 $record['tag'] = $this->read_tag();\r
107                 $record['checksum'] = array($this->read_ushort(),$this->read_ushort());\r
108                 $record['offset'] = $this->read_ulong();\r
109                 $record['length'] = $this->read_ulong();\r
110                 $this->tables[$record['tag']] = $record;\r
111                 }\r
112                 if (!$presubset) $this->checksumTables();\r
113         }\r
114 \r
115         function checksumTables() {\r
116                 // Check the checksums for all tables\r
117                 foreach($this->tables AS $t) {\r
118                   if ($t['length'] > 0 && $t['length'] < $this->maxStrLenRead) {        // 1.02\r
119                 $table = $this->get_chunk($t['offset'], $t['length']);\r
120                 $checksum = $this->calcChecksum($table);\r
121                 if ($t['tag'] == 'head') {\r
122                                 $up = unpack('n*', substr($table,8,4));\r
123                                 $adjustment[0] = $up[1];\r
124                                 $adjustment[1] = $up[2];\r
125                         $checksum = $this->sub32($checksum, $adjustment);\r
126                         }\r
127                 $xchecksum = $t['checksum'];\r
128                 if ($xchecksum != $checksum) \r
129                     die(sprintf('TTF file "%s": invalid checksum %s table: %s (expected %s)', $this->filename,dechex($checksum[0]).dechex($checksum[1]),$t['tag'],dechex($xchecksum[0]).dechex($xchecksum[1])));\r
130                   }\r
131                 }\r
132         }\r
133 \r
134         function sub32($x, $y) {\r
135                 $xlo = $x[1];\r
136                 $xhi = $x[0];\r
137                 $ylo = $y[1];\r
138                 $yhi = $y[0];\r
139                 if ($ylo > $xlo) { $xlo += 1 << 16; $yhi += 1; }\r
140                 $reslo = $xlo-$ylo;\r
141                 if ($yhi > $xhi) { $xhi += 1 << 16;  }\r
142                 $reshi = $xhi-$yhi;\r
143                 $reshi = $reshi & 0xFFFF;\r
144                 return array($reshi, $reslo);\r
145         }\r
146 \r
147         function calcChecksum($data)  {\r
148                 if (strlen($data) % 4) { $data .= str_repeat("\0",(4-(strlen($data) % 4))); }\r
149                 $hi=0x0000;\r
150                 $lo=0x0000;\r
151                 for($i=0;$i<strlen($data);$i+=4) {\r
152                         $hi += (ord($data[$i])<<8) + ord($data[$i+1]);\r
153                         $lo += (ord($data[$i+2])<<8) + ord($data[$i+3]);\r
154                         $hi += $lo >> 16;\r
155                         $lo = $lo & 0xFFFF;\r
156                         $hi = $hi & 0xFFFF;\r
157                 }\r
158                 return array($hi, $lo);\r
159         }\r
160 \r
161         function get_table_pos($tag) {\r
162                 $offset = $this->tables[$tag]['offset'];\r
163                 $length = $this->tables[$tag]['length'];\r
164                 return array($offset, $length);\r
165         }\r
166 \r
167         function seek($pos) {\r
168                 $this->_pos = $pos;\r
169                 fseek($this->fh,$this->_pos);\r
170         }\r
171 \r
172         function skip($delta) {\r
173                 $this->_pos = $this->_pos + $delta;\r
174                 fseek($this->fh,$this->_pos);\r
175         }\r
176 \r
177         function seek_table($tag, $offset_in_table = 0) {\r
178                 $tpos = $this->get_table_pos($tag);\r
179                 $this->_pos = $tpos[0] + $offset_in_table;\r
180                 fseek($this->fh, $this->_pos);\r
181                 return $this->_pos;\r
182         }\r
183 \r
184         function read_tag() {\r
185                 $this->_pos += 4;\r
186                 return fread($this->fh,4);\r
187         }\r
188 \r
189         function read_short() {\r
190                 $this->_pos += 2;\r
191                 $s = fread($this->fh,2);\r
192                 $a = (ord($s[0])<<8) + ord($s[1]);\r
193                 if ($a & (1 << 15) ) { $a = ($a - (1 << 16)) ; }\r
194                 return $a;\r
195         }\r
196 \r
197         function read_ushort() {\r
198                 $this->_pos += 2;\r
199                 $s = fread($this->fh,2);\r
200                 return (ord($s[0])<<8) + ord($s[1]);\r
201         }\r
202 \r
203         function read_ulong() {\r
204                 $this->_pos += 4;\r
205                 $s = fread($this->fh,4);\r
206                 // if large uInt32 as an integer, PHP converts it to -ve\r
207                 return (ord($s[0])*16777216) + (ord($s[1])<<16) + (ord($s[2])<<8) + ord($s[3]); //      16777216  = 1<<24\r
208         }\r
209 \r
210         function get_ushort($pos) {\r
211                 fseek($this->fh,$pos);\r
212                 $s = fread($this->fh,2);\r
213                 return (ord($s[0])<<8) + ord($s[1]);\r
214         }\r
215 \r
216         function get_ulong($pos) {\r
217                 fseek($this->fh,$pos);\r
218                 $s = fread($this->fh,4);\r
219                 // iF large uInt32 as an integer, PHP converts it to -ve\r
220                 return (ord($s[0])*16777216) + (ord($s[1])<<16) + (ord($s[2])<<8) + ord($s[3]); //      16777216  = 1<<24\r
221         }\r
222 \r
223         function pack_short($val) {\r
224                 if ($val<0) { \r
225                         $val = abs($val);\r
226                         $val = ~$val;\r
227                         $val += 1;\r
228                 }\r
229                 return pack("n",$val); \r
230         }\r
231 \r
232         function splice($stream, $offset, $value) {\r
233                 return substr($stream,0,$offset) . $value . substr($stream,$offset+strlen($value));\r
234         }\r
235 \r
236         function _set_ushort($stream, $offset, $value) {\r
237                 $up = pack("n", $value);\r
238                 return $this->splice($stream, $offset, $up);\r
239         }\r
240 \r
241         function get_chunk($pos, $length) {\r
242                 fseek($this->fh,$pos);\r
243                 if ($length <1) { return ''; }  // 1.02\r
244                 return (fread($this->fh,$length));\r
245         }\r
246 \r
247         function get_table($tag) {\r
248                 list($pos, $length) = $this->get_table_pos($tag);\r
249                 if ($length == 0) { die('Truetype font ('.$this->filename.'): error reading table: '.$tag); }   // 1.02\r
250                 fseek($this->fh,$pos);\r
251                 return (fread($this->fh,$length));\r
252         }\r
253 \r
254         function add($tag, $data) {\r
255                 if ($tag == 'head') {\r
256                         $data = $this->splice($data, 8, "\0\0\0\0");\r
257                 }\r
258                 $this->otables[$tag] = $data;\r
259         }\r
260 \r
261 \r
262 \r
263 /////////////////////////////////////////////////////////////////////////////////////////\r
264 \r
265 \r
266         function extractInfo($presubset=0) {\r
267                 // = 0 ; validate; does not need glyphPos or charToGlyph\r
268                 // presubset = 1; no validation; does not need name and other metrics\r
269                 ///////////////////////////////////\r
270                 // name - Naming table\r
271                 ///////////////////////////////////\r
272                 if (!$presubset) { \r
273                         $name_offset = $this->seek_table("name");\r
274                         $format = $this->read_ushort();\r
275                         if ($format != 0)\r
276                                 die("Unknown name table format ".$format);\r
277                         $numRecords = $this->read_ushort();\r
278                         $string_data_offset = $name_offset + $this->read_ushort();\r
279 \r
280                         $names = array(1=>'',2=>'',3=>'',4=>'',6=>'');\r
281                         $K = array_keys($names);\r
282                         $nameCount = count($names);\r
283                         for ($i=0;$i<$numRecords; $i++) {\r
284                                 $platformId = $this->read_ushort();\r
285                                 $encodingId = $this->read_ushort();\r
286                                 $languageId = $this->read_ushort();\r
287                                 $nameId = $this->read_ushort();\r
288                                 $length = $this->read_ushort();\r
289                                 $offset = $this->read_ushort();\r
290                                 if (!in_array($nameId,$K)) continue;\r
291                                 $N = '';\r
292                                 if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name\r
293                                         $opos = $this->_pos;\r
294                                         $this->seek($string_data_offset + $offset);\r
295                                         if ($length % 2 != 0)\r
296                                                 die("PostScript name is UTF-16BE string of odd length");\r
297                                         $length /= 2;\r
298                                         $N = '';\r
299                                         while ($length > 0) {\r
300                                                 $char = $this->read_ushort();\r
301                                                 $N .= (chr($char));     // 1.02\r
302                                                 $length -= 1;\r
303                                         }\r
304                                         $this->_pos = $opos;\r
305                                         $this->seek($opos);\r
306                                 }\r
307                                 else if ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name\r
308                                         $opos = $this->_pos;\r
309                                         $N = $this->get_chunk($string_data_offset + $offset, $length);\r
310                                         $this->_pos = $opos;\r
311                                         $this->seek($opos);\r
312                                 }\r
313                                 if ($N && $names[$nameId]=='') {\r
314                                         $names[$nameId] = $N;\r
315                                         $nameCount -= 1;\r
316                                         if ($nameCount==0) break;\r
317                                 }\r
318                         }\r
319                         if ($names[6])\r
320                                 $psName = preg_replace('/ /','-',$names[6]);\r
321                         else if ($names[4])\r
322                                 $psName = preg_replace('/ /','-',$names[4]);\r
323                         else if ($names[1])\r
324                                 $psName = preg_replace('/ /','-',$names[1]);\r
325                         else\r
326                                 $psName = '';\r
327                         if (!$psName)\r
328                                 die("Could not find PostScript font name");\r
329                         for ($i=0;$i<strlen($psName);$i++) {\r
330                                 $c = $psName[$i];       // 1.02\r
331                                 $oc = ord($c);\r
332                                 if ($oc>126 || strpos(' [](){}<>/%',$c)!==false)\r
333                                         die("psName=".$psName." contains invalid character ".$c." ie U+".ord(c));\r
334                         }\r
335                         $this->name = $psName;\r
336                         if ($names[1]) { $this->familyName = $names[1]; } else { $this->familyName = $psName; }\r
337                         if ($names[2]) { $this->styleName = $names[2]; } else { $this->styleName = 'Regular'; }\r
338                         if ($names[4]) { $this->fullName = $names[4]; } else { $this->fullName = $psName; }\r
339                         if ($names[3]) { $this->uniqueFontID = $names[3]; } else { $this->uniqueFontID = $psName; }\r
340                 }\r
341 \r
342                 ///////////////////////////////////\r
343                 // head - Font header table\r
344                 ///////////////////////////////////\r
345                 $this->seek_table("head");\r
346                 if ($presubset) { $this->skip(18); }\r
347                 else {\r
348                         $ver_maj = $this->read_ushort();\r
349                         $ver_min = $this->read_ushort();\r
350                         if ($ver_maj != 1)\r
351                                 die('Unknown head table version '. $ver_maj .'.'. $ver_min);\r
352                         $this->fontRevision = $this->read_ushort() . $this->read_ushort();\r
353 \r
354                         $this->skip(4);\r
355                         $magic = $this->read_ulong();\r
356                         if ($magic != 0x5F0F3CF5) \r
357                                 die('Invalid head table magic ' .$magic);\r
358                         $this->skip(2);\r
359                 }\r
360                 $this->unitsPerEm = $unitsPerEm = $this->read_ushort(); // 1.02\r
361                 $scale = 1000 / $unitsPerEm;    // 1.02\r
362                 if ($presubset) { $this->skip(30); }    // 1.02\r
363                 else { \r
364                         $this->skip(16);\r
365                         $xMin = $this->read_short();\r
366                         $yMin = $this->read_short();\r
367                         $xMax = $this->read_short();\r
368                         $yMax = $this->read_short();\r
369                         $this->bbox = array(($xMin*$scale), ($yMin*$scale), ($xMax*$scale), ($yMax*$scale));\r
370                         $this->skip(3*2);\r
371                 }\r
372                 $indexToLocFormat = $this->read_ushort();\r
373                 $glyphDataFormat = $this->read_ushort();\r
374                 if ($glyphDataFormat != 0)\r
375                         die('Unknown glyph data format '.$glyphDataFormat);\r
376 \r
377                 ///////////////////////////////////\r
378                 // hhea metrics table\r
379                 ///////////////////////////////////\r
380                 // ttf2t1 seems to use this value rather than the one in OS/2 - so put in for compatibility\r
381                 if (isset($this->tables["hhea"])) {\r
382                         $this->seek_table("hhea");\r
383                         $this->skip(4);\r
384                         $hheaAscender = $this->read_short();\r
385                         $hheaDescender = $this->read_short();\r
386                         $this->ascent = ($hheaAscender *$scale);\r
387                         $this->descent = ($hheaDescender *$scale);\r
388                 }\r
389 \r
390                 ///////////////////////////////////\r
391                 // OS/2 - OS/2 and Windows metrics table\r
392                 ///////////////////////////////////\r
393                 if (isset($this->tables["OS/2"])) {\r
394                         $this->seek_table("OS/2");\r
395                         $version = $this->read_ushort();\r
396                         $this->skip(2);\r
397                         $usWeightClass = $this->read_ushort();\r
398                         $this->skip(2);\r
399                         $fsType = $this->read_ushort();\r
400         //              if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) \r
401         //                      die('Font does not allow subsetting/embedding');\r
402                         $this->skip(58);   //11*2 + 10 + 4*4 + 4 + 3*2\r
403                         $sTypoAscender = $this->read_short();\r
404                         $sTypoDescender = $this->read_short();\r
405                         if (!$this->ascent) $this->ascent = ($sTypoAscender*$scale);\r
406                         if (!$this->descent) $this->descent = ($sTypoDescender*$scale);\r
407                         if ($version > 1) {\r
408                                 $this->skip(16);\r
409                                 $sCapHeight = $this->read_short();\r
410                                 $this->capHeight = ($sCapHeight*$scale);\r
411                         }\r
412                         else {\r
413                                 $this->capHeight = $this->ascent;\r
414                         }\r
415                 }\r
416                 else {\r
417                         $usWeightClass = 500;\r
418                         if (!$this->ascent) $this->ascent = ($yMax*$scale);\r
419                         if (!$this->descent) $this->descent = ($yMin*$scale);\r
420                         $this->capHeight = $this->ascent;\r
421                 }\r
422                 $this->stemV = 50 + intval(pow(($usWeightClass / 65.0),2));\r
423 \r
424                 ///////////////////////////////////\r
425                 // post - PostScript table\r
426                 ///////////////////////////////////\r
427                 $this->seek_table("post");\r
428                 if ($presubset) { $this->skip(4); }\r
429                 else {\r
430                         $ver_maj = $this->read_ushort();\r
431                         $ver_min = $this->read_ushort();\r
432                         if ($ver_maj <1 || $ver_maj >4) \r
433                                 die('Unknown post table version '.$ver_maj);\r
434                 }\r
435                 $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0;\r
436                 $this->underlinePosition = $this->read_short() * $scale;\r
437                 $this->underlineThickness = $this->read_short() * $scale;\r
438                 $isFixedPitch = $this->read_ulong();\r
439 \r
440                 $this->flags = 4;\r
441 \r
442                 if ($this->italicAngle!= 0) \r
443                         $this->flags = $this->flags | 64;\r
444                 if ($usWeightClass >= 600)\r
445                         $this->flags = $this->flags | 262144;\r
446                 if ($isFixedPitch)\r
447                         $this->flags = $this->flags | 1;\r
448 \r
449                 ///////////////////////////////////\r
450                 // hhea - Horizontal header table\r
451                 ///////////////////////////////////\r
452                 $this->seek_table("hhea");\r
453                 if ($presubset) { $this->skip(32); }\r
454                 else {\r
455                         $ver_maj = $this->read_ushort();\r
456                         $ver_min = $this->read_ushort();\r
457                         if ($ver_maj != 1)\r
458                                 die('Unknown hhea table version '.$ver_maj);\r
459                         $this->skip(28);\r
460                 }\r
461                 $metricDataFormat = $this->read_ushort();\r
462                 if ($metricDataFormat != 0)\r
463                         die('Unknown horizontal metric data format '.$metricDataFormat);\r
464                 $numberOfHMetrics = $this->read_ushort();\r
465                 if ($numberOfHMetrics == 0) \r
466                         die('Number of horizontal metrics is 0');\r
467 \r
468                 ///////////////////////////////////\r
469                 // maxp - Maximum profile table\r
470                 ///////////////////////////////////\r
471                 $this->seek_table("maxp");\r
472                 if ($presubset) { $this->skip(4); }\r
473                 else {\r
474                         $ver_maj = $this->read_ushort();\r
475                         $ver_min = $this->read_ushort();\r
476                         if ($ver_maj != 1)\r
477                                 die('Unknown maxp table version '.$ver_maj);\r
478                 }\r
479                 $numGlyphs = $this->read_ushort();\r
480 \r
481                 ///////////////////////////////////\r
482                 // cmap - Character to glyph index mapping table\r
483                 ///////////////////////////////////\r
484                 $cmap_offset = $this->seek_table("cmap");\r
485                 $this->skip(2);\r
486                 $cmapTableCount = $this->read_ushort();\r
487                 $unicode_cmap_offset = 0;\r
488                 for ($i=0;$i<$cmapTableCount;$i++) {\r
489                         $platformID = $this->read_ushort();\r
490                         $encodingID = $this->read_ushort();\r
491                         $offset = $this->read_ulong();\r
492                         $save_pos = $this->_pos;\r
493                         if ($platformID == 3 && $encodingID == 1) { // Microsoft, Unicode\r
494                                 $format = $this->get_ushort($cmap_offset + $offset);\r
495                                 if ($format == 4) {\r
496                                         $unicode_cmap_offset = $cmap_offset + $offset;\r
497                                         break;\r
498                                 }\r
499                         }\r
500                         else if ($platformID == 0) { // Unicode -- assume all encodings are compatible\r
501                                 $format = $this->get_ushort($cmap_offset + $offset);\r
502                                 if ($format == 4) {\r
503                                         $unicode_cmap_offset = $cmap_offset + $offset;\r
504                                         break;\r
505                                 }\r
506                         }\r
507                         $this->seek($save_pos );\r
508                 }\r
509 \r
510                 if (!$unicode_cmap_offset)\r
511                         die('Font does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)');\r
512                 $this->seek($unicode_cmap_offset + 2);\r
513                 $length = $this->read_ushort();\r
514                 $limit = $unicode_cmap_offset + $length;\r
515                 $this->skip(2);\r
516 \r
517                 $segCount = $this->read_ushort() / 2;\r
518                 $this->skip(6);\r
519                 $endCount = array();\r
520                 for($i=0; $i<$segCount; $i++) { $endCount[] = $this->read_ushort(); }\r
521                 $this->skip(2);\r
522                 $startCount = array();\r
523                 for($i=0; $i<$segCount; $i++) { $startCount[] = $this->read_ushort(); }\r
524                 $idDelta = array();\r
525                 for($i=0; $i<$segCount; $i++) { $idDelta[] = $this->read_short(); }             // ???? was unsigned short\r
526                 $idRangeOffset_start = $this->_pos;\r
527                 $idRangeOffset = array();\r
528                 for($i=0; $i<$segCount; $i++) { $idRangeOffset[] = $this->read_ushort(); }\r
529 \r
530                 $glyphToChar = array();\r
531                 $charToGlyph = array();\r
532                 for ($n=0;$n<$segCount;$n++) {\r
533                         for ($unichar=$startCount[$n];$unichar<($endCount[$n] + 1);$unichar++) {\r
534                                 if ($idRangeOffset[$n] == 0)\r
535                                         $glyph = ($unichar + $idDelta[$n]) & 0xFFFF;\r
536                                 else {\r
537                                         $offset = ($unichar - $startCount[$n]) * 2 + $idRangeOffset[$n];\r
538                                         $offset = $idRangeOffset_start + 2 * $n + $offset;\r
539                                         if ($offset >= $limit)\r
540                                                 $glyph = 0;\r
541                                         else {\r
542                                                 $glyph = $this->get_ushort($offset);\r
543                                                 if ($glyph != 0)\r
544                                                 $glyph = ($glyph + $idDelta[$n]) & 0xFFFF;\r
545                                         }\r
546                                 }\r
547                                 if ($presubset) $charToGlyph[$unichar] = $glyph;\r
548                                 $glyphToChar[$glyph][] = $unichar;\r
549                         }\r
550                 }\r
551                 if ($presubset) $this->charToGlyph = $charToGlyph;\r
552 \r
553                 ///////////////////////////////////\r
554                 // hmtx - Horizontal metrics table\r
555                 ///////////////////////////////////\r
556                 $this->seek_table("hmtx");\r
557                 $aw = 0;\r
558                 $this->charWidths = array();\r
559                 $this->hmetrics = array();\r
560                 for( $glyph=0; $glyph<$numberOfHMetrics; $glyph++) {\r
561                         $aw = $this->read_ushort();\r
562                         $lsb = $this->read_short();\r
563                         $this->hmetrics[] = array($aw, $lsb);\r
564                         $aw = $scale*$aw;\r
565                         if ($glyph == 0)\r
566                                 $this->defaultWidth = $aw;\r
567                         if (isset($glyphToChar[$glyph])) {\r
568                                 foreach($glyphToChar[$glyph] AS $char) {\r
569                                         $this->charWidths[$char] = round($aw);\r
570                                 }\r
571                         }\r
572                 }\r
573                 for( $glyph=$numberOfHMetrics; $glyph<$numGlyphs; $glyph++) {\r
574                         $lsb = $this->read_ushort();\r
575                         $this->hmetrics[] = array($aw, $lsb);\r
576                         if (isset($glyphToChar[$glyph])) {\r
577                                 foreach($glyphToChar[$glyph] AS $char) {\r
578                                         $this->charWidths[$char] = round($aw);\r
579                                 }\r
580                         }\r
581                 }\r
582 \r
583                 ///////////////////////////////////\r
584                 // loca - Index to location\r
585                 ///////////////////////////////////\r
586                 if ($presubset) {\r
587                         $this->seek_table('loca');\r
588                         $this->glyphPos = array();\r
589                         if ($indexToLocFormat == 0) {\r
590                                 for ($n=0; $n<=$numGlyphs; $n++) {      // 1.02\r
591                                         $this->glyphPos[] = ($this->read_ushort() * 2);\r
592                                 }\r
593                         }\r
594                         else if ($indexToLocFormat == 1) {\r
595                                 for ($n=0; $n<=$numGlyphs; $n++) {      // 1.02\r
596                                         $this->glyphPos[] = ($this->read_ulong());\r
597                                 }\r
598                         }\r
599                         else \r
600                                 die('Unknown location table format '.$indexToLocFormat);\r
601                 }\r
602         }\r
603 \r
604 \r
605 /////////////////////////////////////////////////////////////////////////////////////////\r
606 \r
607 \r
608         function makeSubset($subset) {\r
609                 $this->fh = fopen($this->filename ,'rb') or die('Can\'t open file ' . $this->filename );\r
610                 $this->otables = array();\r
611                 $glyphMap = array(0=>0); \r
612                 $glyphSet = array(0=>0);\r
613                 $codeToGlyph = array();\r
614                 foreach($subset AS $code) {\r
615                         if (isset($this->charToGlyph[$code]))\r
616                                 $originalGlyphIdx = $this->charToGlyph[$code];\r
617                         else\r
618                                 $originalGlyphIdx = 0;\r
619                         if (!isset($glyphSet[$originalGlyphIdx])) {\r
620                                 $glyphSet[$originalGlyphIdx] = count($glyphMap);\r
621                                 $glyphMap[] = $originalGlyphIdx;\r
622                         }\r
623                         $codeToGlyph[$code] = $glyphSet[$originalGlyphIdx];\r
624                 }\r
625 \r
626                 list($start,$dummy) = $this->get_table_pos('glyf');\r
627 \r
628                 $n = 0;\r
629                 while ($n < count($glyphMap)) {\r
630                         $originalGlyphIdx = $glyphMap[$n];\r
631                         $glyphPos = $this->glyphPos[$originalGlyphIdx];\r
632                         $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;\r
633                         $n += 1;\r
634                         if (!$glyphLen) continue;\r
635                         $this->seek($start + $glyphPos);\r
636                         $numberOfContours = $this->read_short();\r
637                         if ($numberOfContours < 0) {\r
638                                 $this->skip(8);\r
639                                 $flags = GF_MORE;\r
640                                 while ($flags & GF_MORE) {\r
641                                         $flags = $this->read_ushort();\r
642                                         $glyphIdx = $this->read_ushort();\r
643                                         if (!isset($glyphSet[$glyphIdx])) {\r
644                                                 $glyphSet[$glyphIdx] = count($glyphMap);\r
645                                                 $glyphMap[] = $glyphIdx;\r
646                                         }\r
647                                         if ($flags & GF_WORDS)\r
648                                                 $this->skip(4);\r
649                                         else\r
650                                                 $this->skip(2);\r
651                                         if ($flags & GF_SCALE)\r
652                                                 $this->skip(2);\r
653                                         else if ($flags & GF_XYSCALE)\r
654                                                 $this->skip(4);\r
655                                         else if ($flags & GF_TWOBYTWO)\r
656                                                 $this->skip(8);\r
657                                 }\r
658                         }\r
659                 }\r
660 \r
661                 $numGlyphs = $n = count($glyphMap);\r
662                 while ($n > 1 && $this->hmetrics[$n][0] == $this->hmetrics[$n - 1][0]) { $n -= 1; }\r
663                 $numberOfHMetrics = $n;\r
664 \r
665                 //tables copied from the original\r
666                 $tags = array ('name', 'OS/2', 'prep');\r
667                 foreach($tags AS $tag) { $this->add($tag, $this->get_table($tag)); }\r
668                 $tags = array ('cvt ', 'fpgm');\r
669                 foreach($tags AS $tag) {        // 1.02\r
670                         if (isset($this->table['tag'])) { $this->add($tag, $this->get_table($tag)); }\r
671                 }\r
672 \r
673                 // post - PostScript\r
674                 $opost = $this->get_table('post');\r
675                 $post = "\x00\x03\x00\x00" . substr($opost,4,12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";\r
676                 $this->add('post', $post);\r
677 \r
678                 // hhea - Horizontal Header\r
679                 $hhea = $this->get_table('hhea');\r
680                 $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics);\r
681                 $this->add('hhea', $hhea);\r
682 \r
683                 // maxp - Maximum Profile\r
684                 $maxp = $this->get_table('maxp');\r
685                 $maxp = $this->_set_ushort($maxp, 4, $numGlyphs);\r
686                 $this->add('maxp', $maxp);\r
687 \r
688                 // cmap - Character to glyph mapping - Format 6\r
689                 $entryCount = count($subset);\r
690                 $length = 10 + $entryCount * 2;\r
691                 $cmap = array(0, 1,     // Index : version, number of subtables\r
692                         1, 0,                   // Subtable : platform, encoding\r
693                         0, 12,          // offset (hi,lo)\r
694                         6, $length,     // Format 6 Mapping table: format, length\r
695                         0, 1,                   // language, First char code\r
696                         $entryCount);\r
697 \r
698                 foreach($subset AS $code) { $cmap[] = $codeToGlyph[$code]; }\r
699                 $cmapstr = '';\r
700                 foreach($cmap AS $cm) { $cmapstr .= pack("n",$cm); }\r
701                 $this->add('cmap', $cmapstr);\r
702 \r
703 /*\r
704                 // cmap - Character to glyph mapping - Format 4 (MS / )\r
705                 $segCount = 2;\r
706                 $searchRange = 1;\r
707                 $entrySelector = 0;\r
708                 while ($searchRange * 2 <= $segCount ) {\r
709                         $searchRange = $searchRange * 2;\r
710                         $entrySelector = $entrySelector + 1;\r
711                 }\r
712                 $searchRange = $searchRange * 16;\r
713                 $rangeShift = $segCount * 16 - $searchRange;\r
714                 $length = 48 + ($numGlyphs-1);\r
715                 $cmap = array(0, 1,     // Index : version, number of subtables\r
716                         3, 0,                   // Subtable : platform (MS=3), encoding\r
717                         0, 12,          // Subtable : offset (hi,lo)\r
718                         4, $length, 0,  // Format 4 Mapping table: format, length, language\r
719                         $segCount*2,\r
720                         $searchRange,\r
721                         $entrySelector,\r
722                         $rangeShift,\r
723                         $numGlyphs, 0xFFFF,     // endCode(s)\r
724                         0,\r
725                         1, 0xFFFF,                      // startCode(s)\r
726                         0, 1,                           // idDelta(s) Delta for all character codes in segment\r
727                         0, 0);                  // idRangeOffset[segCount]      Offset in bytes to glyph indexArray, or 0\r
728                 foreach($subset AS $code) { $cmap[] = $codeToGlyph[$code]; }\r
729                 $cmap[] = 0xFFFF;\r
730                 $cmapstr = '';\r
731                 foreach($cmap AS $cm) { $cmapstr .= pack("n",$cm); }\r
732                 $this->add('cmap', $cmapstr);\r
733 */\r
734 \r
735 \r
736                 // hmtx - Horizontal Metrics\r
737                 $hmtxstr = '';\r
738                 for($n=0;$n<$numGlyphs;$n++) {\r
739                         $originalGlyphIdx = $glyphMap[$n];\r
740                         $aw = $this->hmetrics[$originalGlyphIdx][0];\r
741                         $lsb = $this->hmetrics[$originalGlyphIdx][1];\r
742                         if ($n < $numberOfHMetrics) { $hmtxstr .= pack("n",$aw); }\r
743                         $hmtxstr .= $this->pack_short($lsb);\r
744                 }\r
745                 $this->add('hmtx', $hmtxstr);\r
746 \r
747                 // glyf - Glyph data\r
748                 list($glyfOffset,$glyfLength) = $this->get_table_pos('glyf');\r
749                 if ($glyfLength < $this->maxStrLenRead) {\r
750                         $glyphData = $this->get_table('glyf');\r
751                 }\r
752 \r
753                 $offsets = array();\r
754                 $glyf = '';\r
755                 $pos = 0;\r
756 \r
757                 for ($n=0;$n<$numGlyphs;$n++) {\r
758                         $offsets[] = $pos;\r
759                         $originalGlyphIdx = $glyphMap[$n];\r
760                         $glyphPos = $this->glyphPos[$originalGlyphIdx];\r
761                         $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;\r
762                         if ($glyfLength < $this->maxStrLenRead) {\r
763                                 $data = substr($glyphData,$glyphPos,$glyphLen);\r
764                         }\r
765                         else {\r
766                                 if ($glyphLen > 0) $data = $this->get_chunk($glyfOffset+$glyphPos,$glyphLen);\r
767                                 else $data = '';\r
768                         }\r
769                         if ($glyphLen > 0) $up = unpack("n", substr($data,0,2));\r
770                         if ($glyphLen > 2 && ($up[1] & (1 << 15)) ) {\r
771                                 $pos_in_glyph = 10;\r
772                                 $flags = GF_MORE;\r
773                                 while ($flags & GF_MORE) {\r
774                                         $up = unpack("n", substr($data,$pos_in_glyph,2));\r
775                                         $flags = $up[1];\r
776                                         $up = unpack("n", substr($data,$pos_in_glyph+2,2));\r
777                                         $glyphIdx = $up[1];\r
778                                         $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]);\r
779                                         $pos_in_glyph += 4;\r
780                                         if ($flags & GF_WORDS) { $pos_in_glyph += 4; }\r
781                                         else { $pos_in_glyph += 2; }\r
782                                         if ($flags & GF_SCALE) { $pos_in_glyph += 2; }\r
783                                         else if ($flags & GF_XYSCALE) { $pos_in_glyph += 4; }\r
784                                         else if ($flags & GF_TWOBYTWO) { $pos_in_glyph += 8; }\r
785                                 }\r
786                         }\r
787                         $glyf .= $data;\r
788                         $pos += $glyphLen;\r
789                         if ($pos % 4 != 0) {\r
790                                 $padding = 4 - ($pos % 4);\r
791                                 $glyf .= str_repeat("\0",$padding);\r
792                                 $pos += $padding;\r
793                         }\r
794                 }\r
795                 $offsets[] = $pos;\r
796                 $this->add('glyf', $glyf);\r
797 \r
798                 // loca - Index to location\r
799                 $locastr = '';\r
800                 if ((($pos + 1) >> 1) > 0xFFFF) {\r
801                         $indexToLocFormat = 1;        // long format\r
802                         foreach($offsets AS $offset) { $locastr .= pack("N",$offset); }\r
803                 }\r
804                 else {\r
805                         $indexToLocFormat = 0;        // short format\r
806                         foreach($offsets AS $offset) { $locastr .= pack("n",($offset/2)); }\r
807                 }\r
808                 $this->add('loca', $locastr);\r
809 \r
810                 // head - Font header\r
811                 $head = $this->get_table('head');\r
812                 $head = $this->_set_ushort($head, 50, $indexToLocFormat);\r
813                 $this->add('head', $head);\r
814 \r
815                 fclose($this->fh);\r
816 \r
817                 // Put the TTF file together\r
818                 $stm = '';\r
819                 $numTables = count($this->otables);\r
820                 $searchRange = 1;\r
821                 $entrySelector = 0;\r
822                 while ($searchRange * 2 <= $numTables) {\r
823                         $searchRange = $searchRange * 2;\r
824                         $entrySelector = $entrySelector + 1;\r
825                 }\r
826                 $searchRange = $searchRange * 16;\r
827                 $rangeShift = $numTables * 16 - $searchRange;\r
828 \r
829                 // Header\r
830                 $stm .= (pack("Nnnnn", 0x74727565, $numTables, $searchRange, $entrySelector, $rangeShift));     // 0x74727565 "true" for Mac\r
831                 // 0x00010000 for Windows\r
832 \r
833                 // Table directory\r
834                 $tables = $this->otables;\r
835                 ksort ($tables); \r
836                 $offset = 12 + $numTables * 16;\r
837                 foreach ($tables AS $tag=>$data) {\r
838                         if ($tag == 'head') { $head_start = $offset; }\r
839                         $stm .= $tag;\r
840                         $checksum = $this->calcChecksum($data);\r
841                         $stm .= pack("nn", $checksum[0],$checksum[1]);\r
842                         $stm .= pack("NN", $offset, strlen($data));\r
843                         $paddedLength = (strlen($data)+3)&~3;\r
844                         $offset = $offset + $paddedLength;\r
845                 }\r
846 \r
847                 // Table data\r
848                 foreach ($tables AS $tag=>$data) {\r
849                         $data .= "\0\0\0";\r
850                         $stm .= substr($data,0,(strlen($data)&~3));\r
851                 }\r
852 \r
853                 $checksum = $this->calcChecksum($stm);\r
854                 $checksum = $this->sub32(array(0xB1B0,0xAFBA), $checksum);\r
855                 $chk = pack("nn", $checksum[0],$checksum[1]);\r
856                 $stm = $this->splice($stm,($head_start + 8),$chk);\r
857                 return $stm ;\r
858         }\r
859 \r
860 \r
861 }\r
862 \r
863 \r
864 ?>