1<?php
2
3define('OT_CMAP_PLATFORM_WINDOWS', 3);
4define('OT_CMAP_PLATFORM_WINDOWS_UNICODE', 1);
5
6define('OT_PLATFORM_ID_MICROSOFT', 3);
7
8define('OT_NAME_ID_SUBFAMILY_NAME', 2);
9define('OT_NAME_ID_UNIQUE_ID', 3);
10define('OT_NAME_ID_FULL_NAME', 4);
11define('OT_NAME_ID_POSTSCRIPT_NAME', 6);
12
13define('OT_CMAP_LANGUAGE_WINDOWS_ENGLISH_AMERICAN', 0x0409);
14
15/**
16 * This class allows parsing TrueType/OpenType font files
17 */
18class OpenTypeFile {
19  var $_filehandle;
20  var $_sfnt;
21
22  function OpenTypeFile() {
23    $this->_filehandle = null;
24    $this->_sfnt = new OpenTypeFileSFNT();
25  }
26
27  function open($filename) {
28    $this->_filehandle = fopen($filename, 'rb');
29    $this->_read($this->_filehandle);
30  }
31
32  function close() {
33    fclose($this->_filehandle);
34  }
35
36  function _delete() {
37    $this->close();
38    $this->_sfnt->_delete();
39  }
40
41  function getFileHandle() {
42    return $this->_filehandle;
43  }
44
45  function &getTable($tag) {
46    $table =& $this->_sfnt->_getTable($tag, $this->_filehandle, $this);
47    return $table;
48  }
49
50  function &_getCMAPSubtable($offset) {
51    $table =& $this->_sfnt->_getCMAPSubtable($offset, $this->_filehandle, $this);
52    return $table;
53  }
54
55  function _read($filehandle) {
56    $this->_sfnt->_read($filehandle);
57  }
58}
59
60/**
61 * A key  characteristic of the  OpenType format is the  TrueType sfnt
62 * "wrapper", which  provides organization for a  collection of tables
63 * in a general and extensible manner.
64 */
65class OpenTypeFileSFNT {
66  var $_offsetTable;
67  var $_tableDirectory;
68
69  var $_tables;
70
71  function _delete() {
72    foreach ($this->_tables as $key => $value) {
73      $this->_tables[$key]->_delete();
74      unset($this->_tables[$key]);
75    };
76    $this->_tables = array();
77  }
78
79  function OpenTypeFileSFNT() {
80    $this->_offsetTable = new OpenTypeFileOffsetTable();
81    $this->_tableDirectory = array();
82  }
83
84  function _read($filehandle) {
85    $this->_offsetTable->_read($filehandle);
86
87    for ($i=0; $i<$this->_offsetTable->_numTables; $i++) {
88      $tableDirectory = new OpenTypeFileTableDirectory();
89      $tableDirectory->_read($filehandle);
90      $this->_tableDirectory[] = $tableDirectory;
91    };
92  }
93
94  function &_getCMAPSubtable($offset, $filehandle, $file) {
95    $dir = $this->_getDirectory('cmap');
96    if (is_null($dir)) { $dummy = null; return $dummy; };
97
98    /**
99     * Store  current  file  position,  as _getCMAPSubtable  could  be
100     * called from another file-related operation
101     */
102    $old_pos = ftell($filehandle);
103
104    fseek($filehandle, $dir->_offset, SEEK_SET);
105    fseek($filehandle, $offset, SEEK_CUR);
106    $subtable = new OpenTypeFileCMAPSubtable();
107    $subtable->_read($filehandle);
108
109    /**
110     * Restore current file position
111     */
112    fseek($filehandle, $old_pos, SEEK_SET);
113
114    return $subtable;
115  }
116
117  function &_getTable($tag, $filehandle, $file) {
118    if (!isset($this->_tables[$tag])) {
119      $table = $this->_createTableByTag($tag);
120      if (is_null($table)) { $dummy = null; return $dummy; };
121      $table->setFontFile($file);
122
123      $dir = $this->_getDirectory($tag);
124      if (is_null($dir)) { $dummy = null; return $dummy; };
125
126      /**
127       * Store  current file  position, as  _getTable could  be called
128       * from another _getTable
129       */
130      $old_pos = ftell($filehandle);
131
132      fseek($filehandle, $dir->_offset, SEEK_SET);
133      $table->_read($filehandle);
134
135      /**
136       * Restore current file position
137       */
138      fseek($filehandle, $old_pos, SEEK_SET);
139
140      $this->_tables[$tag] =& $table;
141    };
142
143    return $this->_tables[$tag];
144  }
145
146  function _getDirectory($tag) {
147    foreach ($this->_tableDirectory as $directoryEntry) {
148      if ($directoryEntry->_tag == $tag) {
149        return $directoryEntry;
150      };
151    };
152
153    return null;
154  }
155
156  function _createTableByTag($tag) {
157    switch ($tag) {
158    case 'hhea':
159      return new OpenTypeFileHHEA();
160    case 'maxp':
161      return new OpenTypeFileMAXP();
162    case 'cmap':
163      return new OpenTypeFileCMAP();
164    case 'hmtx':
165      return new OpenTypeFileHMTX();
166    case 'post':
167      return new OpenTypeFilePOST();
168    case 'head':
169      return new OpenTypeFileHEAD();
170    case 'name':
171      return new OpenTypeFileNAME();
172    default:
173      return null;
174    }
175  }
176}
177
178/**
179 * The OpenType font with the Offset Table. If the font file contains only one font, the Offset Table will begin at byte 0 of the file. If the font file is a TrueType collection, the beginning point of the Offset Table for each font is indicated in the TTCHeader.
180 *
181 * Offset Table Type 	Name 	Description
182 * Fixed 	sfnt version 	0x00010000 for version 1.0.
183 * USHORT 	numTables 	Number of tables.
184 * USHORT 	searchRange 	(Maximum power of 2 <= numTables) x 16.
185 * USHORT 	entrySelector 	Log2(maximum power of 2 <= numTables).
186 * USHORT 	rangeShift 	NumTables x 16-searchRange.
187 *
188 * OpenType fonts that contain  TrueType outlines should use the value
189 * of 1.0  for the  sfnt version. OpenType  fonts containing  CFF data
190 * should use the tag 'OTTO' as the sfnt version number.
191 *
192 * NOTE: The Apple specification  for TrueType fonts allows for 'true'
193 * and 'typ1' for sfnt version.  These version tags should not be used
194 * for fonts which contain OpenType tables.
195 */
196class OpenTypeFileOffsetTable {
197  var $_numTables;
198  var $_searchRange;
199  var $_entrySelector;
200  var $_rangeShift;
201
202  function OpenTypeFileOffsetTable() {
203    $this->_numTables     = 0;
204    $this->_searchRange   = 0;
205    $this->_entrySelector = 0;
206    $this->_rangeShift    = 0;
207  }
208
209  function _read($filehandle) {
210    $content = fread($filehandle, 4+4*2);
211
212    $unpacked = unpack("Nversion/nnumTables/nsearchRange/nentrySelector/nrangeShift", $content);
213
214    $fixed                = $unpacked['version'];
215    $this->_numTables     = $unpacked['numTables'];
216    $this->_searchRange   = $unpacked['searchRange'];
217    $this->_entrySelector = $unpacked['entrySelector'];
218    $this->_rangeShift    = $unpacked['rangeShift'];
219  }
220}
221
222/**
223 * The  Offset Table is  followed immediately  by the  Table Directory
224 * entries. Entries in the Table Directory must be sorted in ascending
225 * order by  tag. Offset  values in the  Table Directory  are measured
226 * from the start of the font file.
227 *
228 * Table Directory Type 	Name 	Description
229 * ULONG 	tag 	4 -byte identifier.
230 * ULONG 	checkSum 	CheckSum for this table.
231 * ULONG 	offset 	Offset from beginning of TrueType font file.
232 * ULONG 	length 	Length of this table.
233 *
234 * The Table Directory  makes it possible for a  given font to contain
235 * only  those tables  it  actually needs.  As  a result  there is  no
236 * standard value for numTables.
237 *
238 * Tags are the  names given to tables in the  OpenType font file. All
239 * tag names  consist of  four characters. Names  with less  than four
240 * letters  are   allowed  if  followed  by   the  necessary  trailing
241 * spaces. All  tag names  defined within a  font (e.g.,  table names,
242 * feature tags, language tags) must be built from printing characters
243 * represented by ASCII values 32-126.
244 */
245class OpenTypeFileTableDirectory {
246  var $_tag;
247  var $_checkSum;
248  var $_offset;
249  var $_length;
250
251  function OpenTypeFileTableDirectory() {
252    $this->_tag      = null;
253    $this->_checkSum = 0;
254    $this->_offset   = 0;
255    $this->_length   = 0;
256  }
257
258  function _read($filehandle) {
259    $content = fread($filehandle, 4*4);
260
261    $unpacked = unpack("c4tag/NcheckSum/Noffset/Nlength", $content);
262
263    $this->_tag      = chr($unpacked['tag1']).chr($unpacked['tag2']).chr($unpacked['tag3']).chr($unpacked['tag4']);
264    $this->_checkSum = $unpacked['checkSum'];
265    $this->_offset   = $unpacked['offset'];
266    $this->_length   = $unpacked['length'];
267  }
268}
269
270/* -------------- */
271
272class OpenTypeFileTable {
273  var $_fontFile;
274
275  function _delete() {
276  }
277
278  function OpenTypeFileTable() {
279    $this->_fontFile = null;
280  }
281
282  function setFontFile(&$fontFile) {
283    $this->_fontFile =& $fontFile;
284  }
285
286  function &getFontFile() {
287    return $this->_fontFile;
288  }
289
290  function _fixFWord($value) {
291    if ($value > 65536/2) {
292      return $value - 65536;
293    } else {
294      return $value;
295    };
296  }
297
298  function _fixShort($value) {
299    if ($value > 65536/2) {
300      return $value - 65536;
301    } else {
302      return $value;
303    };
304  }
305}
306
307class OpenTypeFilePOST extends OpenTypeFileTable {
308  var $_version;
309  var $_italicAngle;
310  var $_underlinePosition;
311  var $_underlineThickness;
312  var $_isFixedPitch;
313  var $_minMemType42;
314  var $_maxMemType42;
315  var $_minMemType1;
316  var $_maxMemType1;
317
318  function OpenTypeFilePOST() {
319    $this->OpenTypeFileTable();
320  }
321
322  function _read($filehandle) {
323    $content  = fread($filehandle, 2*2 + 7*4);
324    $unpacked = unpack("Nversion/NitalicAngle/nunderlinePosition/nunderlineThickness/NisFixedPitch/NminMemType42/NmaxMemType42/NminMemType1/NmaxMemType1", $content);
325    $this->_version            = $unpacked['version'];
326    $this->_italicAngle        = $unpacked['italicAngle'];
327    $this->_underlinePosition  = $this->_fixFWord($unpacked['underlinePosition']);
328    $this->_underlineThickness = $this->_fixFWord($unpacked['underlineThickness']);
329    $this->_isFixedPitch       = $unpacked['isFixedPitch'];
330    $this->_minMemType42       = $unpacked['minMemType42'];
331    $this->_maxMemType42       = $unpacked['maxMemType42'];
332    $this->_minMemType1        = $unpacked['minMemType1'];
333    $this->_maxMemType1        = $unpacked['maxMemType1'];
334  }
335}
336
337class OpenTypeFileNAME extends OpenTypeFileTable {
338  var $_format;
339  var $_count;
340  var $_stringOffset;
341  var $_nameRecord;
342
343  function OpenTypeFileNAME() {
344    $this->OpenTypeFileTable();
345    $this->_nameRecord = array();
346  }
347
348  function _read($filehandle) {
349    $content  = fread($filehandle, 2*3);
350    $unpacked = unpack("nformat/ncount/nstringOffset", $content);
351
352    $this->_format       = $unpacked['format'];
353    $this->_count        = $unpacked['count'];
354    $this->_stringOffset = $unpacked['stringOffset'];
355
356    $baseOffset = ftell($filehandle) + OpenTypeFileNAMERecord::sizeof()*$this->_count;
357
358    for ($i=0; $i<$this->_count; $i++) {
359      $record =& new OpenTypeFileNAMERecord();
360      $record->setBaseOffset($baseOffset);
361      $record->setFontFile($this->getFontFile());
362      $record->_read($filehandle);
363      $this->_nameRecord[] =& $record;
364    };
365  }
366
367  /**
368   * Note that this function can perform "wildcard" lookups when one or more
369   * parameters is set to null value; in this case the first encountered name
370   * will be returned
371   *
372   * @return String corresponding name content or null is this name is
373   * not defined in the font file
374   */
375  function lookup($platformId, $encodingId, $languageId, $nameId) {
376    $size = count($this->_nameRecord);
377
378    for ($i=0; $i<$size; $i++) {
379      if ($this->_nameRecord[$i]->match($platformId, $encodingId, $languageId, $nameId)) {
380        return $this->_nameRecord[$i]->getName();
381      };
382    }
383
384    return null;
385  }
386}
387
388class OpenTypeFileNAMERecord extends OpenTypeFileTable {
389  var $_platformId;
390  var $_encodingId;
391  var $_languageId;
392  var $_nameId;
393  var $_length;
394  var $_offset;
395
396  var $_content;
397  var $_baseOffset;
398
399  function OpenTypeFileNAMERecord() {
400    $this->OpenTypeFileTable();
401    $this->_content = null;
402  }
403
404  function sizeof() {
405    return 6*2;
406  }
407
408  function setBaseOffset($offset) {
409    $this->_baseOffset = $offset;
410  }
411
412  function match($platformId, $encodingId, $languageId, $nameId) {
413    return
414      (is_null($platformId) || $platformId == $this->_platformId) &&
415      (is_null($encodingId) || $encodingId == $this->_encodingId) &&
416      (is_null($languageId) || $languageId == $this->_languageId) &&
417      (is_null($nameId)     || $nameId     == $this->_nameId);
418  }
419
420  function getBaseOffset() {
421    return $this->_baseOffset;
422  }
423
424  function getName() {
425    if (is_null($this->_content)) {
426      $file =& $this->getFontFile();
427      $filehandle = $file->getFileHandle();
428      $old_offset = ftell($filehandle);
429
430      fseek($filehandle, $this->getBaseOffset() + $this->_offset, SEEK_SET);
431      $this->_content = fread($filehandle, $this->_length);
432
433      fseek($filehandle, $old_offset, SEEK_SET);
434    };
435
436    return $this->_content;
437  }
438
439  function _read($filehandle) {
440    $content = fread($filehandle, 6*2);
441
442    $unpacked = unpack("nplatformId/nencodingId/nlanguageId/nnameId/nlength/noffset", $content);
443
444    $this->_platformId    = $unpacked['platformId'];
445    $this->_encodingId    = $unpacked['encodingId'];
446    $this->_languageId    = $unpacked['languageId'];
447    $this->_nameId        = $unpacked['nameId'];
448    $this->_length        = $unpacked['length'];
449    $this->_offset        = $unpacked['offset'];
450  }
451}
452
453/**
454 * This table  gives global information  about the font.  The bounding
455 * box  values  should  be   computed  using  only  glyphs  that  have
456 * contours.  Glyphs  with  no  contours  should be  ignored  for  the
457 * purposes of these calculations.
458 *
459 * Type 	Name 	Description
460 * Fixed 	Table version number 	0x00010000 for version 1.0.
461 * Fixed 	fontRevision 	Set by font manufacturer.
462 * ULONG 	checkSumAdjustment 	To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum.
463 * ULONG 	magicNumber 	Set to 0x5F0F3CF5.
464 * USHORT 	flags 	Bit 0: Baseline for font at y=0;
465 * Bit 1: Left sidebearing point at x=0;
466 * Bit 2: Instructions may depend on point size;
467 * Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear;
468 * Bit 4: Instructions may alter advance width (the advance widths might not scale linearly);
469 * Bits 5-10: These should be set according to Apple's specification . However, they are not implemented in OpenType.
470 * Bit 11: Font data is 'lossless,' as a result of having been compressed and decompressed with the Agfa MicroType Express engine.
471 * Bit 12: Font converted (produce compatible metrics)
472 * Bit 13: Font optimised for ClearType
473 * Bit 14: Reserved, set to 0
474 * Bit 15: Reserved, set to 0
475 * USHORT 	unitsPerEm 	Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines.
476 * LONGDATETIME 	created 	Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
477 * LONGDATETIME 	modified 	Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
478 * SHORT 	xMin 	For all glyph bounding boxes.
479 * SHORT 	yMin 	For all glyph bounding boxes.
480 * SHORT 	xMax 	For all glyph bounding boxes.
481 * SHORT 	yMax 	For all glyph bounding boxes.
482 * USHORT 	macStyle 	Bit 0: Bold (if set to 1);
483 * Bit 1: Italic (if set to 1)
484 * Bit 2: Underline (if set to 1)
485 * Bit 3: Outline (if set to 1)
486 * Bit 4: Shadow (if set to 1)
487 * Bit 5: Condensed (if set to 1)
488 * Bit 6: Extended (if set to 1)
489 * Bits 7-15: Reserved (set to 0).
490 * USHORT 	lowestRecPPEM 	Smallest readable size in pixels.
491 * SHORT 	fontDirectionHint 	0: Fully mixed directional glyphs;
492 * 1: Only strongly left to right;
493 * 2: Like 1 but also contains neutrals;
494 * -1: Only strongly right to left;
495 * -2: Like -1 but also contains neutrals. 1
496 * SHORT 	indexToLocFormat 	0 for short offsets, 1 for long.
497 * SHORT 	glyphDataFormat 	0 for current format.
498 */
499class OpenTypeFileHEAD extends OpenTypeFileTable {
500  var $_version;
501  var $_fontRevision;
502  var $_checkSumAdjustment;
503  var $_magicNumber;
504  var $_flags;
505  var $_unitsPerEm;
506  var $_created;
507  var $_modified;
508  var $_xMin;
509  var $_yMin;
510  var $_xMax;
511  var $_yMax;
512  var $_macStyle;
513  var $_lowestRecPPEM;
514  var $_fontDirectionHint;
515  var $_indexToLocFormat;
516  var $_glyphDataFormat;
517
518  function OpenTypeFileHEAD() {
519    $this->OpenTypeFileTable();
520  }
521
522  function _read($filehandle) {
523    $content = fread($filehandle, 4*4 + 11*2 + 2*8);
524
525    $unpacked = unpack("Nversion/NfontRevision/NcheckSumAdjustment/NmagicNumber/nflags/nunitsPerEm/N2created/N2modified/nxMin/nyMin/nxMax/nyMax/nmacStyle/nlowestRecPPEM/nfontDirectionHint/nindexToLocFormat/nglyphDataFormat", $content);
526    $this->_version            = $unpacked['version'];
527    $this->_fontRevision       = $unpacked['fontRevision'];
528    $this->_checkSumAdjustment = $unpacked['checkSumAdjustment'];
529    $this->_magicNumber        = $unpacked['magicNumber'];
530    $this->_flags              = $unpacked['flags'];
531    $this->_unitsPerEm         = $unpacked['unitsPerEm'];
532    $this->_created            = $unpacked['created1']  << 32 | $unpacked['created2'];
533    $this->_modified           = $unpacked['modified1'] << 32 | $unpacked['modified2'];
534    $this->_xMin               = $this->_fixShort($unpacked['xMin']);
535    $this->_yMin               = $this->_fixShort($unpacked['yMin']);
536    $this->_xMax               = $this->_fixShort($unpacked['xMax']);
537    $this->_yMax               = $this->_fixShort($unpacked['yMax']);
538    $this->_macStyle           = $unpacked['macStyle'];
539    $this->_lowestRecPPEM      = $unpacked['lowestRecPPEM'];
540    $this->_fontDirectionHint  = $this->_fixShort($unpacked['fontDirectionHint']);
541    $this->_indexToLocFormat   = $this->_fixShort($unpacked['indexToLocFormat']);
542    $this->_glyphDataFormat    = $this->_fixShort($unpacked['glyphDataFormat']);
543  }
544}
545
546class OpenTypeFileCMAP extends OpenTypeFileTable {
547  var $_header;
548  var $_encodings;
549  var $_subtables;
550
551  function OpenTypeFileCMAP() {
552    $this->OpenTypeFileTable();
553    $this->_header = new OpenTypeFileCMAPHeader();
554    $this->_encodings = array();
555    $this->_subtables = array();
556  }
557
558  function _read($filehandle) {
559    $this->_header->_read($filehandle);
560
561    for ($i=0; $i<$this->_header->_numTables; $i++) {
562      $encoding = new OpenTypeFileCMAPEncoding();
563      $encoding->_read($filehandle);
564      $this->_encodings[] =& $encoding;
565    };
566  }
567
568  /**
569   * It is assumed that current  file position is set to the beginning
570   * of CMAP table
571   */
572  function _getSubtable($filehandle, $offset) {
573    fseek($filehandle, $offset, SEEK_CUR);
574
575    $subtable = new OpenTypeFileCMAPSubtable();
576    $subtable->_read($filehandle);
577
578    return $subtable;
579  }
580
581  function &findSubtable($platformId, $encodingId) {
582    $file = $this->getFontFile();
583
584    $index = 0;
585    foreach ($this->_encodings as $encoding) {
586      if ($encoding->_platformId == $platformId &&
587          $encoding->_encodingId == $encodingId) {
588        return $this->getSubtable($index);
589      };
590    };
591
592    $dummy = null; return $dummy;
593  }
594
595  function &getSubtable($index) {
596    if (!isset($this->_subtables[$index])) {
597      $file =& $this->getFontFile();
598      $subtable =& $file->_getCMAPSubtable($this->_encodings[$index]->_offset);
599      $this->_subtables[$index] =& $subtable;
600      return $subtable;
601    } else {
602      return $this->_subtables[$index];
603    };
604  }
605}
606
607/**
608 * TODO: support for CMAP subtable formats other than 4
609 */
610class OpenTypeFileCMAPSubtable {
611  var $_format;
612  var $_content;
613
614  function OpenTypeFileCMAPSubtable() {
615    $this->_content = null;
616  }
617
618  function lookup($unicode) {
619    return $this->_content->lookup($unicode);
620  }
621
622  function _read($filehandle) {
623    $content = fread($filehandle, 2);
624
625    $unpacked = unpack("nformat", $content);
626    $this->_format = $unpacked['format'];
627
628    switch ($this->_format) {
629    case 4:
630      $this->_content = new OpenTypeFileCMAPSubtable4();
631      $this->_content->_read($filehandle);
632      return;
633
634    default:
635      die(sprintf("Unsupported CMAP subtable format: %i", $this->_format));
636    }
637  }
638}
639
640class OpenTypeFileCMAPSubtable4 extends OpenTypeFileTable {
641  var $_length;
642  var $_language;
643  var $_segCountX2;
644  var $_searchRange;
645  var $_entrySelector;
646  var $_rangeShift;
647  var $_endCount;
648  var $_startCount;
649  var $_idDelta;
650  var $_idRangeOffset;
651  var $_glyphIdArray;
652
653  function OpenTypeFileCMAPSubtable4() {
654    $this->_endCount      = array();
655    $this->_startCount    = array();
656    $this->_idDelta       = array();
657    $this->_idRangeOffset = array();
658    $this->_glyphIdArray  = array();
659  }
660
661  function lookup($unicode) {
662    $index = $this->_lookupSegment($unicode);
663    if (is_null($index)) { return null; };
664
665    if ($this->_idRangeOffset[$index] != 0) {
666      /**
667       * If  the idRangeOffset  value for  the segment  is not  0, the
668       * mapping  of  character  codes  relies  on  glyphIdArray.  The
669       * character  code  offset  from   startCode  is  added  to  the
670       * idRangeOffset value. This  sum is used as an  offset from the
671       * current location within idRangeOffset itself to index out the
672       * correct glyphIdArray value. This obscure indexing trick works
673       * because glyphIdArray immediately follows idRangeOffset in the
674       * font file. The C expression that yields the glyph index is:
675       *
676       * *(idRangeOffset[i]/2 + (c - startCount[i]) + &idRangeOffset[i])
677       *
678       * The value c  is the character code in question,  and i is the
679       * segment index in which c  appears. If the value obtained from
680       * the   indexing   operation   is   not  0   (which   indicates
681       * missingGlyph),  idDelta[i] is added  to it  to get  the glyph
682       * index. The idDelta arithmetic is modulo 65536.
683       */
684      $value = $this->_glyphIdArray[$unicode - $this->_startCount[$index]];
685      return ($value + $this->_idDelta[$index]) % 65536;
686
687    } else {
688      /**
689       * If  the  idRangeOffset  is  0,  the idDelta  value  is  added
690       * directly to  the character code offset (i.e.  idDelta[i] + c)
691       * to  get the  corresponding  glyph index.  Again, the  idDelta
692       * arithmetic is modulo 65536.
693       */
694      return ($this->_idDelta[$index] + $unicode) % 65536;
695    };
696  }
697
698  /**
699   * The segments  are sorted in  order of increasing  endCode values,
700   * and the segment values are specified in four parallel arrays. You
701   * search for the first endCode that is greater than or equal to the
702   * character code you want to map.
703   */
704  function _lookupSegment($unicode) {
705    for ($i=0; $i<$this->_segCountX2/2; $i++) {
706      if ($unicode <= $this->_endCount[$i]) {
707        /**
708         * If the corresponding startCode is less than or equal to the
709         * character code, then you  use the corresponding idDelta and
710         * idRangeOffset to  map the character  code to a  glyph index
711         * (otherwise, the missingGlyph is returned).
712         */
713        if ($this->_startCount[$i] <= $unicode) {
714          return $i;
715        } else {
716          return null;
717        };
718      };
719    };
720    return null;
721  }
722
723  function _read($filehandle) {
724    $content = fread($filehandle, 6*2);
725    $unpacked = unpack("nlength/nlanguage/nsegCountX2/nsearchRange/nentrySelector/nrangeShift", $content);
726    $this->_length        = $unpacked['length'];
727    $this->_language      = $unpacked['language'];
728    $this->_segCountX2    = $unpacked['segCountX2'];
729    $this->_searchRange   = $unpacked['searchRange'];
730    $this->_entrySelector = $unpacked['entrySelector'];
731    $this->_rangeShift    = $unpacked['rangeShift'];
732
733    for ($i=0; $i<floor($this->_segCountX2/2); $i++) {
734      $content = fread($filehandle, 2);
735      $unpacked = unpack("nendCount", $content);
736      $this->_endCount[] = $unpacked['endCount'];
737    };
738
739    // Skip 'reservedPad' field
740    $content = fread($filehandle, 2);
741
742    for ($i=0; $i<$this->_segCountX2/2; $i++) {
743      $content = fread($filehandle, 2);
744      $unpacked = unpack("nstartCount", $content);
745      $this->_startCount[] = $unpacked['startCount'];
746    };
747
748    for ($i=0; $i<$this->_segCountX2/2; $i++) {
749      $content = fread($filehandle, 2);
750      $unpacked = unpack("nidDelta", $content);
751      $this->_idDelta[] = $this->_fixShort($unpacked['idDelta']);
752    };
753
754    for ($i=0; $i<$this->_segCountX2/2; $i++) {
755      $content = fread($filehandle, 2);
756      $unpacked = unpack("nidRangeOffset", $content);
757      $this->_idRangeOffset[] = $unpacked['idRangeOffset'];
758    };
759
760    for ($i=0; $i<$this->_length - 2*12; $i+=2) {
761      $content = fread($filehandle, 2);
762      $unpacked = unpack("nglyphId", $content);
763      $this->_glyphIdArray[] = $unpacked['glyphId'];
764    };
765  }
766}
767
768class OpenTypeFileCMAPEncoding {
769  var $_platformId;
770  var $_encodingId;
771  var $_offset;
772
773  function _read($filehandle) {
774    $content = fread($filehandle, 2*2+4);
775
776    $unpacked = unpack("nplatformId/nencodingId/Noffset", $content);
777    $this->_platformId = $unpacked['platformId'];
778    $this->_encodingId = $unpacked['encodingId'];
779    $this->_offset     = $unpacked['offset'];
780  }
781}
782
783class OpenTypeFileCMAPHeader {
784  var $_version;
785  var $_numTables;
786
787  function _read($filehandle) {
788    $content = fread($filehandle, 2*2);
789
790    $unpacked = unpack("nversion/nnumTables", $content);
791    $this->_version   = $unpacked['version'];
792    $this->_numTables = $unpacked['numTables'];
793  }
794}
795
796// @TODO: v 1.0 support
797class OpenTypeFileMAXP extends OpenTypeFileTable {
798  var $_numGlyphs;
799
800  function OpenTypeFileMAXP() {
801    $this->OpenTypeFileTable();
802  }
803
804  function _read($filehandle) {
805    $content = fread($filehandle, 4+2*1);
806
807    $unpacked = unpack("Nversion/nnumGlyphs", $content);
808
809    $version          = $unpacked['version'];
810    $this->_numGlyphs = $unpacked['numGlyphs'];
811  }
812}
813
814class OpenTypeFileHHEA extends OpenTypeFileTable {
815  var $_ascender;
816  var $_descender;
817  var $_lineGap;
818  var $_advanceWidthMax;
819  var $_minLeftSideBearing;
820  var $_minRightSideBearing;
821  var $_xMaxExtent;
822  var $_caretSlopeRise;
823  var $_caretSlopeRun;
824  var $_caretOffset;
825  var $_metricDataFormat;
826  var $_numberOfHMetrics;
827
828  function OpenTypeFileHHEA() {
829    $this->OpenTypeFileTable();
830  }
831
832  function _read($filehandle) {
833    $content = fread($filehandle, 4+16*2);
834
835    $unpacked = unpack("Nversion/nascender/ndescender/nlineGap/nadvanceWidthMax/nminLeftSideBearing/".
836                       "nminRightSideBearing/nxMaxExtent/ncaretSlopeRise/ncaretSlopeRun/ncaretOffset/n4reserved/".
837                       "nmetricDataFormat/nnumberOfHMetrics", $content);
838
839    $version                    = $unpacked['version'];
840    $this->_ascender            = $this->_fixFWord($unpacked['ascender']);
841    $this->_descender           = $this->_fixFWord($unpacked['descender']);
842    $this->_lineGap             = $this->_fixFWord($unpacked['lineGap']);
843    $this->_advanceWidthMax     = $unpacked['advanceWidthMax'];
844    $this->_minLeftSideBearing  = $this->_fixFWord($unpacked['minLeftSideBearing']);
845    $this->_minRightSideBearing = $this->_fixFWord($unpacked['minRightSideBearing']);
846    $this->_xMaxExtent          = $this->_fixFWord($unpacked['xMaxExtent']);
847    $this->_caretSlopeRise      = $this->_fixShort($unpacked['caretSlopeRise']);
848    $this->_caretSlopeRun       = $this->_fixShort($unpacked['caretSlopeRun']);
849    $this->_caretOffset         = $this->_fixShort($unpacked['caretOffset']);
850    $this->_metricDataFormat    = $this->_fixShort($unpacked['metricDataFormat']);
851    $this->_numberOfHMetrics    = $unpacked['numberOfHMetrics'];
852  }
853}
854
855class OpenTypeFileHMTX extends OpenTypeFileTable {
856  var $_hMetrics;
857  var $_leftSideBearing;
858
859  function _delete() {
860    unset($this->_hMetrics);
861    unset($this->_leftSideBearing);
862  }
863
864  function OpenTypeFileHMTX() {
865    $this->OpenTypeFileTable();
866
867    $this->_hMetrics        = array();
868    $this->_leftSideBearing = array();
869  }
870
871  function _read($filehandle) {
872    $fontFile =& $this->getFontFile();
873    $hhea =& $fontFile->getTable('hhea');
874    $maxp =& $fontFile->getTable('maxp');
875
876    for ($i=0; $i<$hhea->_numberOfHMetrics; $i++) {
877      $content = fread($filehandle, 2*2);
878      $unpacked = unpack("nadvanceWidth/nlsb", $content);
879      $this->_hMetrics[] = array('advanceWidth' => $unpacked['advanceWidth'],
880                                 'lsb'          => $this->_fixShort($unpacked['lsb']));
881    };
882
883    for ($i=0; $i<$maxp->_numGlyphs; $i++) {
884      $content = fread($filehandle, 2);
885      $unpacked = unpack("nitem", $content);
886      $this->_leftSideBearing[] = $unpacked['item'];
887    };
888  }
889}
890
891
892?>