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