<?php

define('OT_CMAP_PLATFORM_WINDOWS', 3);
define('OT_CMAP_PLATFORM_WINDOWS_UNICODE', 1);

define('OT_PLATFORM_ID_MICROSOFT', 3);

define('OT_NAME_ID_SUBFAMILY_NAME', 2);
define('OT_NAME_ID_UNIQUE_ID', 3);
define('OT_NAME_ID_FULL_NAME', 4);
define('OT_NAME_ID_POSTSCRIPT_NAME', 6);

define('OT_CMAP_LANGUAGE_WINDOWS_ENGLISH_AMERICAN', 0x0409);

/**
 * This class allows parsing TrueType/OpenType font files
 */
class OpenTypeFile {
  var $_filehandle;
  var $_sfnt;

  function OpenTypeFile() {
    $this->_filehandle = null;
    $this->_sfnt = new OpenTypeFileSFNT();
  }
  
  function open($filename) {
    $this->_filehandle = fopen($filename, 'rb');
    $this->_read($this->_filehandle);
  }

  function close() {
    fclose($this->_filehandle);
  }

  function _delete() {
    $this->close();
    $this->_sfnt->_delete();
  }

  function getFileHandle() {
    return $this->_filehandle;
  }

  function &getTable($tag) {
    $table =& $this->_sfnt->_getTable($tag, $this->_filehandle, $this);   
    return $table;
  }

  function &_getCMAPSubtable($offset) {
    $table =& $this->_sfnt->_getCMAPSubtable($offset, $this->_filehandle, $this);
    return $table;
  }

  function _read($filehandle) {
    $this->_sfnt->_read($filehandle);
  }
}

/**
 * A key  characteristic of the  OpenType format is the  TrueType sfnt
 * "wrapper", which  provides organization for a  collection of tables
 * in a general and extensible manner.
 */
class OpenTypeFileSFNT {
  var $_offsetTable;
  var $_tableDirectory;

  var $_tables;

  function _delete() {
    foreach ($this->_tables as $key => $value) {
      $this->_tables[$key]->_delete();
      unset($this->_tables[$key]);
    };
    $this->_tables = array();
  }

  function OpenTypeFileSFNT() {
    $this->_offsetTable = new OpenTypeFileOffsetTable();
    $this->_tableDirectory = array();
  }

  function _read($filehandle) {
    $this->_offsetTable->_read($filehandle);

    for ($i=0; $i<$this->_offsetTable->_numTables; $i++) {
      $tableDirectory = new OpenTypeFileTableDirectory();
      $tableDirectory->_read($filehandle);
      $this->_tableDirectory[] = $tableDirectory;
    };
  }

  function &_getCMAPSubtable($offset, $filehandle, $file) {
    $dir = $this->_getDirectory('cmap');
    if (is_null($dir)) { $dummy = null; return $dummy; };
    
    /**
     * Store  current  file  position,  as _getCMAPSubtable  could  be
     * called from another file-related operation
     */
    $old_pos = ftell($filehandle);
    
    fseek($filehandle, $dir->_offset, SEEK_SET);
    fseek($filehandle, $offset, SEEK_CUR);
    $subtable = new OpenTypeFileCMAPSubtable();
    $subtable->_read($filehandle);
    
    /**
     * Restore current file position
     */
    fseek($filehandle, $old_pos, SEEK_SET);
    
    return $subtable;
  }

  function &_getTable($tag, $filehandle, $file) {
    if (!isset($this->_tables[$tag])) {
      $table = $this->_createTableByTag($tag);
      if (is_null($table)) { $dummy = null; return $dummy; };
      $table->setFontFile($file);

      $dir = $this->_getDirectory($tag);
      if (is_null($dir)) { $dummy = null; return $dummy; };

      /**
       * Store  current file  position, as  _getTable could  be called
       * from another _getTable
       */
      $old_pos = ftell($filehandle);

      fseek($filehandle, $dir->_offset, SEEK_SET);
      $table->_read($filehandle);

      /**
       * Restore current file position
       */
      fseek($filehandle, $old_pos, SEEK_SET);

      $this->_tables[$tag] =& $table;
    };

    return $this->_tables[$tag];
  }

  function _getDirectory($tag) {
    foreach ($this->_tableDirectory as $directoryEntry) {
      if ($directoryEntry->_tag == $tag) {
        return $directoryEntry;
      };
    };

    return null;
  }

  function _createTableByTag($tag) {
    switch ($tag) {
    case 'hhea':
      return new OpenTypeFileHHEA();
    case 'maxp':
      return new OpenTypeFileMAXP();
    case 'cmap':
      return new OpenTypeFileCMAP();
    case 'hmtx':
      return new OpenTypeFileHMTX();
    case 'post':
      return new OpenTypeFilePOST();
    case 'head':
      return new OpenTypeFileHEAD();
    case 'name':
      return new OpenTypeFileNAME();
    default:
      return null;
    }
  }
}

/**
 * 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.
 * 
 * Offset Table Type 	Name 	Description
 * Fixed 	sfnt version 	0x00010000 for version 1.0.
 * USHORT 	numTables 	Number of tables.
 * USHORT 	searchRange 	(Maximum power of 2 <= numTables) x 16.
 * USHORT 	entrySelector 	Log2(maximum power of 2 <= numTables).
 * USHORT 	rangeShift 	NumTables x 16-searchRange.
 *
 * OpenType fonts that contain  TrueType outlines should use the value
 * of 1.0  for the  sfnt version. OpenType  fonts containing  CFF data
 * should use the tag 'OTTO' as the sfnt version number.
 *
 * NOTE: The Apple specification  for TrueType fonts allows for 'true'
 * and 'typ1' for sfnt version.  These version tags should not be used
 * for fonts which contain OpenType tables.
 */
class OpenTypeFileOffsetTable {
  var $_numTables;
  var $_searchRange;
  var $_entrySelector;
  var $_rangeShift;

  function OpenTypeFileOffsetTable() {
    $this->_numTables     = 0;
    $this->_searchRange   = 0;
    $this->_entrySelector = 0;
    $this->_rangeShift    = 0;
  }

  function _read($filehandle) {
    $content = fread($filehandle, 4+4*2);    

    $unpacked = unpack("Nversion/nnumTables/nsearchRange/nentrySelector/nrangeShift", $content);

    $fixed                = $unpacked['version'];
    $this->_numTables     = $unpacked['numTables'];
    $this->_searchRange   = $unpacked['searchRange'];
    $this->_entrySelector = $unpacked['entrySelector'];
    $this->_rangeShift    = $unpacked['rangeShift'];
  }
}

/**
 * The  Offset Table is  followed immediately  by the  Table Directory
 * entries. Entries in the Table Directory must be sorted in ascending
 * order by  tag. Offset  values in the  Table Directory  are measured
 * from the start of the font file.
 *
 * Table Directory Type 	Name 	Description
 * ULONG 	tag 	4 -byte identifier.
 * ULONG 	checkSum 	CheckSum for this table.
 * ULONG 	offset 	Offset from beginning of TrueType font file.
 * ULONG 	length 	Length of this table.
 *
 * The Table Directory  makes it possible for a  given font to contain
 * only  those tables  it  actually needs.  As  a result  there is  no
 * standard value for numTables.
 *
 * Tags are the  names given to tables in the  OpenType font file. All
 * tag names  consist of  four characters. Names  with less  than four
 * letters  are   allowed  if  followed  by   the  necessary  trailing
 * spaces. All  tag names  defined within a  font (e.g.,  table names,
 * feature tags, language tags) must be built from printing characters
 * represented by ASCII values 32-126.
 */
class OpenTypeFileTableDirectory {
  var $_tag;
  var $_checkSum;
  var $_offset;
  var $_length;

  function OpenTypeFileTableDirectory() {
    $this->_tag      = null;
    $this->_checkSum = 0;
    $this->_offset   = 0;
    $this->_length   = 0;
  }

  function _read($filehandle) {
    $content = fread($filehandle, 4*4);

    $unpacked = unpack("c4tag/NcheckSum/Noffset/Nlength", $content);

    $this->_tag      = chr($unpacked['tag1']).chr($unpacked['tag2']).chr($unpacked['tag3']).chr($unpacked['tag4']);
    $this->_checkSum = $unpacked['checkSum'];
    $this->_offset   = $unpacked['offset'];
    $this->_length   = $unpacked['length'];
  }
}

/* -------------- */

class OpenTypeFileTable {
  var $_fontFile;

  function _delete() {
  }

  function OpenTypeFileTable() {
    $this->_fontFile = null;
  }

  function setFontFile(&$fontFile) {
    $this->_fontFile =& $fontFile;
  }

  function &getFontFile() {
    return $this->_fontFile;
  }

  function _fixFWord($value) {
    if ($value > 65536/2) {
      return $value - 65536;
    } else {
      return $value;
    };
  }

  function _fixShort($value) {
    if ($value > 65536/2) {
      return $value - 65536;
    } else {
      return $value;
    };
  }
}

class OpenTypeFilePOST extends OpenTypeFileTable {
  var $_version;
  var $_italicAngle;
  var $_underlinePosition;
  var $_underlineThickness;
  var $_isFixedPitch;
  var $_minMemType42;
  var $_maxMemType42;
  var $_minMemType1;
  var $_maxMemType1;

  function OpenTypeFilePOST() {
    $this->OpenTypeFileTable();
  }

  function _read($filehandle) {
    $content  = fread($filehandle, 2*2 + 7*4);
    $unpacked = unpack("Nversion/NitalicAngle/nunderlinePosition/nunderlineThickness/NisFixedPitch/NminMemType42/NmaxMemType42/NminMemType1/NmaxMemType1", $content);
    $this->_version            = $unpacked['version'];
    $this->_italicAngle        = $unpacked['italicAngle'];
    $this->_underlinePosition  = $this->_fixFWord($unpacked['underlinePosition']);
    $this->_underlineThickness = $this->_fixFWord($unpacked['underlineThickness']);
    $this->_isFixedPitch       = $unpacked['isFixedPitch'];
    $this->_minMemType42       = $unpacked['minMemType42'];
    $this->_maxMemType42       = $unpacked['maxMemType42'];
    $this->_minMemType1        = $unpacked['minMemType1'];
    $this->_maxMemType1        = $unpacked['maxMemType1'];
  }
}

class OpenTypeFileNAME extends OpenTypeFileTable {
  var $_format;
  var $_count;
  var $_stringOffset;
  var $_nameRecord;

  function OpenTypeFileNAME() {
    $this->OpenTypeFileTable();
    $this->_nameRecord = array();
  }

  function _read($filehandle) {
    $content  = fread($filehandle, 2*3);    
    $unpacked = unpack("nformat/ncount/nstringOffset", $content);

    $this->_format       = $unpacked['format'];
    $this->_count        = $unpacked['count'];
    $this->_stringOffset = $unpacked['stringOffset'];

    $baseOffset = ftell($filehandle) + OpenTypeFileNAMERecord::sizeof()*$this->_count;

    for ($i=0; $i<$this->_count; $i++) {
      $record = new OpenTypeFileNAMERecord();
      $record->setBaseOffset($baseOffset);
      $record->setFontFile($this->getFontFile());
      $record->_read($filehandle);
      $this->_nameRecord[] =& $record;
    };
  }

  /**
   * Note that this function can perform "wildcard" lookups when one or more 
   * parameters is set to null value; in this case the first encountered name 
   * will be returned
   *
   * @return String corresponding name content or null is this name is
   * not defined in the font file
   */
  function lookup($platformId, $encodingId, $languageId, $nameId) {
    $size = count($this->_nameRecord);

    for ($i=0; $i<$size; $i++) {
      if ($this->_nameRecord[$i]->match($platformId, $encodingId, $languageId, $nameId)) {
        return $this->_nameRecord[$i]->getName();
      };
    }

    return null;
  }
}

class OpenTypeFileNAMERecord extends OpenTypeFileTable {
  var $_platformId;
  var $_encodingId;
  var $_languageId;
  var $_nameId;
  var $_length;
  var $_offset;

  var $_content;
  var $_baseOffset;

  function OpenTypeFileNAMERecord() {
    $this->OpenTypeFileTable();
    $this->_content = null;
  }

  function sizeof() {
    return 6*2;
  }

  function setBaseOffset($offset) {
    $this->_baseOffset = $offset;
  }

  function match($platformId, $encodingId, $languageId, $nameId) {
    return
      (is_null($platformId) || $platformId == $this->_platformId) &&
      (is_null($encodingId) || $encodingId == $this->_encodingId) &&
      (is_null($languageId) || $languageId == $this->_languageId) &&
      (is_null($nameId)     || $nameId     == $this->_nameId);
  }

  function getBaseOffset() {
    return $this->_baseOffset;
  }

  function getName() {
    if (is_null($this->_content)) {
      $file =& $this->getFontFile();
      $filehandle = $file->getFileHandle();
      $old_offset = ftell($filehandle);

      fseek($filehandle, $this->getBaseOffset() + $this->_offset, SEEK_SET);
      $this->_content = fread($filehandle, $this->_length);
    
      fseek($filehandle, $old_offset, SEEK_SET);
    };

    return $this->_content;
  }

  function _read($filehandle) {
    $content = fread($filehandle, 6*2);

    $unpacked = unpack("nplatformId/nencodingId/nlanguageId/nnameId/nlength/noffset", $content);

    $this->_platformId    = $unpacked['platformId'];
    $this->_encodingId    = $unpacked['encodingId'];
    $this->_languageId    = $unpacked['languageId'];
    $this->_nameId        = $unpacked['nameId'];
    $this->_length        = $unpacked['length'];
    $this->_offset        = $unpacked['offset'];
  }
}

/**
 * This table  gives global information  about the font.  The bounding
 * box  values  should  be   computed  using  only  glyphs  that  have
 * contours.  Glyphs  with  no  contours  should be  ignored  for  the
 * purposes of these calculations.
 *
 * Type 	Name 	Description
 * Fixed 	Table version number 	0x00010000 for version 1.0.
 * Fixed 	fontRevision 	Set by font manufacturer.
 * ULONG 	checkSumAdjustment 	To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum.
 * ULONG 	magicNumber 	Set to 0x5F0F3CF5.
 * USHORT 	flags 	Bit 0: Baseline for font at y=0;
 * Bit 1: Left sidebearing point at x=0;
 * Bit 2: Instructions may depend on point size;
 * Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear;
 * Bit 4: Instructions may alter advance width (the advance widths might not scale linearly);
 * Bits 5-10: These should be set according to Apple's specification . However, they are not implemented in OpenType.
 * Bit 11: Font data is 'lossless,' as a result of having been compressed and decompressed with the Agfa MicroType Express engine.
 * Bit 12: Font converted (produce compatible metrics)
 * Bit 13: Font optimised for ClearType
 * Bit 14: Reserved, set to 0
 * Bit 15: Reserved, set to 0
 * USHORT 	unitsPerEm 	Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines.
 * LONGDATETIME 	created 	Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
 * LONGDATETIME 	modified 	Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
 * SHORT 	xMin 	For all glyph bounding boxes.
 * SHORT 	yMin 	For all glyph bounding boxes.
 * SHORT 	xMax 	For all glyph bounding boxes.
 * SHORT 	yMax 	For all glyph bounding boxes.
 * USHORT 	macStyle 	Bit 0: Bold (if set to 1);
 * Bit 1: Italic (if set to 1)
 * Bit 2: Underline (if set to 1)
 * Bit 3: Outline (if set to 1)
 * Bit 4: Shadow (if set to 1)
 * Bit 5: Condensed (if set to 1)
 * Bit 6: Extended (if set to 1)
 * Bits 7-15: Reserved (set to 0).
 * USHORT 	lowestRecPPEM 	Smallest readable size in pixels.
 * SHORT 	fontDirectionHint 	0: Fully mixed directional glyphs;
 * 1: Only strongly left to right;
 * 2: Like 1 but also contains neutrals;
 * -1: Only strongly right to left;
 * -2: Like -1 but also contains neutrals. 1
 * SHORT 	indexToLocFormat 	0 for short offsets, 1 for long.
 * SHORT 	glyphDataFormat 	0 for current format.
 */
class OpenTypeFileHEAD extends OpenTypeFileTable {
  var $_version;
  var $_fontRevision;
  var $_checkSumAdjustment;
  var $_magicNumber;
  var $_flags;
  var $_unitsPerEm;
  var $_created;
  var $_modified;
  var $_xMin;
  var $_yMin;
  var $_xMax;
  var $_yMax;
  var $_macStyle;
  var $_lowestRecPPEM;
  var $_fontDirectionHint;
  var $_indexToLocFormat;
  var $_glyphDataFormat;

  function OpenTypeFileHEAD() {
    $this->OpenTypeFileTable();
  }

  function _read($filehandle) {
    $content = fread($filehandle, 4*4 + 11*2 + 2*8);
    
    $unpacked = unpack("Nversion/NfontRevision/NcheckSumAdjustment/NmagicNumber/nflags/nunitsPerEm/N2created/N2modified/nxMin/nyMin/nxMax/nyMax/nmacStyle/nlowestRecPPEM/nfontDirectionHint/nindexToLocFormat/nglyphDataFormat", $content);
    $this->_version            = $unpacked['version'];
    $this->_fontRevision       = $unpacked['fontRevision'];
    $this->_checkSumAdjustment = $unpacked['checkSumAdjustment'];
    $this->_magicNumber        = $unpacked['magicNumber'];
    $this->_flags              = $unpacked['flags'];
    $this->_unitsPerEm         = $unpacked['unitsPerEm'];
    $this->_created            = $unpacked['created1']  << 32 | $unpacked['created2'];
    $this->_modified           = $unpacked['modified1'] << 32 | $unpacked['modified2'];
    $this->_xMin               = $this->_fixShort($unpacked['xMin']);
    $this->_yMin               = $this->_fixShort($unpacked['yMin']);
    $this->_xMax               = $this->_fixShort($unpacked['xMax']);
    $this->_yMax               = $this->_fixShort($unpacked['yMax']);
    $this->_macStyle           = $unpacked['macStyle'];
    $this->_lowestRecPPEM      = $unpacked['lowestRecPPEM'];
    $this->_fontDirectionHint  = $this->_fixShort($unpacked['fontDirectionHint']);
    $this->_indexToLocFormat   = $this->_fixShort($unpacked['indexToLocFormat']);
    $this->_glyphDataFormat    = $this->_fixShort($unpacked['glyphDataFormat']);
  }
}

class OpenTypeFileCMAP extends OpenTypeFileTable {
  var $_header;
  var $_encodings;
  var $_subtables;

  function OpenTypeFileCMAP() {
    $this->OpenTypeFileTable();
    $this->_header = new OpenTypeFileCMAPHeader();
    $this->_encodings = array();
    $this->_subtables = array();
  }

  function _read($filehandle) {
    $this->_header->_read($filehandle);

    for ($i=0; $i<$this->_header->_numTables; $i++) {
      $encoding = new OpenTypeFileCMAPEncoding();
      $encoding->_read($filehandle);
      $this->_encodings[] =& $encoding;
    };
  }

  /**
   * It is assumed that current  file position is set to the beginning
   * of CMAP table
   */
  function _getSubtable($filehandle, $offset) {
    fseek($filehandle, $offset, SEEK_CUR);

    $subtable = new OpenTypeFileCMAPSubtable();
    $subtable->_read($filehandle);

    return $subtable;
  }

  function &findSubtable($platformId, $encodingId) {
    $file = $this->getFontFile();

    $index = 0;
    foreach ($this->_encodings as $encoding) {
      if ($encoding->_platformId == $platformId &&
          $encoding->_encodingId == $encodingId) {
        return $this->getSubtable($index);
      };
    };

    $dummy = null; return $dummy;
  }

  function &getSubtable($index) {
    if (!isset($this->_subtables[$index])) {
      $file =& $this->getFontFile(); 
      $subtable =& $file->_getCMAPSubtable($this->_encodings[$index]->_offset);
      $this->_subtables[$index] =& $subtable;
      return $subtable;
    } else {
      return $this->_subtables[$index];
    };
  }
}

/**
 * TODO: support for CMAP subtable formats other than 4
 */
class OpenTypeFileCMAPSubtable {
  var $_format;
  var $_content;

  function OpenTypeFileCMAPSubtable() {
    $this->_content = null;
  }

  function lookup($unicode) { 
    return $this->_content->lookup($unicode); 
  }

  function _read($filehandle) {
    $content = fread($filehandle, 2);
    
    $unpacked = unpack("nformat", $content);
    $this->_format = $unpacked['format'];

    switch ($this->_format) {
    case 4:
      $this->_content = new OpenTypeFileCMAPSubtable4();
      $this->_content->_read($filehandle);
      return;
        
    default:
      die(sprintf("Unsupported CMAP subtable format: %i", $this->_format));
    }
  }
}

class OpenTypeFileCMAPSubtable4 extends OpenTypeFileTable {
  var $_length;
  var $_language;
  var $_segCountX2;
  var $_searchRange;
  var $_entrySelector;
  var $_rangeShift;
  var $_endCount;
  var $_startCount;
  var $_idDelta;
  var $_idRangeOffset;
  var $_glyphIdArray;

  function OpenTypeFileCMAPSubtable4() {
    $this->_endCount      = array();
    $this->_startCount    = array();
    $this->_idDelta       = array();
    $this->_idRangeOffset = array();
    $this->_glyphIdArray  = array();
  }

  function lookup($unicode) {
    $index = $this->_lookupSegment($unicode);
    if (is_null($index)) { return null; };

    if ($this->_idRangeOffset[$index] != 0) {
      /**
       * If  the idRangeOffset  value for  the segment  is not  0, the
       * mapping  of  character  codes  relies  on  glyphIdArray.  The
       * character  code  offset  from   startCode  is  added  to  the
       * idRangeOffset value. This  sum is used as an  offset from the
       * current location within idRangeOffset itself to index out the
       * correct glyphIdArray value. This obscure indexing trick works
       * because glyphIdArray immediately follows idRangeOffset in the
       * font file. The C expression that yields the glyph index is:
       *
       * *(idRangeOffset[i]/2 + (c - startCount[i]) + &idRangeOffset[i])
       *
       * The value c  is the character code in question,  and i is the
       * segment index in which c  appears. If the value obtained from
       * the   indexing   operation   is   not  0   (which   indicates
       * missingGlyph),  idDelta[i] is added  to it  to get  the glyph
       * index. The idDelta arithmetic is modulo 65536.
       */
      $value = $this->_glyphIdArray[$unicode - $this->_startCount[$index]];
      return ($value + $this->_idDelta[$index]) % 65536;

    } else {
      /**
       * If  the  idRangeOffset  is  0,  the idDelta  value  is  added
       * directly to  the character code offset (i.e.  idDelta[i] + c)
       * to  get the  corresponding  glyph index.  Again, the  idDelta
       * arithmetic is modulo 65536.
       */
      return ($this->_idDelta[$index] + $unicode) % 65536;
    };
  }

  /**
   * The segments  are sorted in  order of increasing  endCode values,
   * and the segment values are specified in four parallel arrays. You
   * search for the first endCode that is greater than or equal to the
   * character code you want to map.
   */
  function _lookupSegment($unicode) {
    for ($i=0; $i<$this->_segCountX2/2; $i++) {
      if ($unicode <= $this->_endCount[$i]) {       
        /**
         * If the corresponding startCode is less than or equal to the
         * character code, then you  use the corresponding idDelta and
         * idRangeOffset to  map the character  code to a  glyph index
         * (otherwise, the missingGlyph is returned). 
         */
        if ($this->_startCount[$i] <= $unicode) {
          return $i;
        } else {
          return null;
        };
      };
    };
    return null;
  }

  function _read($filehandle) {
    $content = fread($filehandle, 6*2);
    $unpacked = unpack("nlength/nlanguage/nsegCountX2/nsearchRange/nentrySelector/nrangeShift", $content);
    $this->_length        = $unpacked['length'];
    $this->_language      = $unpacked['language'];
    $this->_segCountX2    = $unpacked['segCountX2'];
    $this->_searchRange   = $unpacked['searchRange'];
    $this->_entrySelector = $unpacked['entrySelector'];
    $this->_rangeShift    = $unpacked['rangeShift'];

    for ($i=0; $i<floor($this->_segCountX2/2); $i++) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nendCount", $content);
      $this->_endCount[] = $unpacked['endCount'];
    };
    
    // Skip 'reservedPad' field
    $content = fread($filehandle, 2);
    
    for ($i=0; $i<$this->_segCountX2/2; $i++) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nstartCount", $content);
      $this->_startCount[] = $unpacked['startCount'];
    };

    for ($i=0; $i<$this->_segCountX2/2; $i++) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nidDelta", $content);
      $this->_idDelta[] = $this->_fixShort($unpacked['idDelta']);
    };

    for ($i=0; $i<$this->_segCountX2/2; $i++) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nidRangeOffset", $content);
      $this->_idRangeOffset[] = $unpacked['idRangeOffset'];
    };

    for ($i=0; $i<$this->_length - 2*12; $i+=2) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nglyphId", $content);
      $this->_glyphIdArray[] = $unpacked['glyphId'];
    };
  }
}

class OpenTypeFileCMAPEncoding {
  var $_platformId;
  var $_encodingId;
  var $_offset;

  function _read($filehandle) {
    $content = fread($filehandle, 2*2+4);
    
    $unpacked = unpack("nplatformId/nencodingId/Noffset", $content);
    $this->_platformId = $unpacked['platformId'];
    $this->_encodingId = $unpacked['encodingId'];
    $this->_offset     = $unpacked['offset'];
  }
}

class OpenTypeFileCMAPHeader {
  var $_version;
  var $_numTables;

  function _read($filehandle) {
    $content = fread($filehandle, 2*2);

    $unpacked = unpack("nversion/nnumTables", $content);
    $this->_version   = $unpacked['version'];
    $this->_numTables = $unpacked['numTables'];
  }
}

// @TODO: v 1.0 support
class OpenTypeFileMAXP extends OpenTypeFileTable {
  var $_numGlyphs;

  function OpenTypeFileMAXP() {
    $this->OpenTypeFileTable();
  }

  function _read($filehandle) {
    $content = fread($filehandle, 4+2*1);
    
    $unpacked = unpack("Nversion/nnumGlyphs", $content);

    $version          = $unpacked['version'];
    $this->_numGlyphs = $unpacked['numGlyphs'];
  }
}

class OpenTypeFileHHEA extends OpenTypeFileTable {
  var $_ascender;
  var $_descender;
  var $_lineGap;
  var $_advanceWidthMax;
  var $_minLeftSideBearing;
  var $_minRightSideBearing;
  var $_xMaxExtent;
  var $_caretSlopeRise;
  var $_caretSlopeRun;
  var $_caretOffset;
  var $_metricDataFormat;
  var $_numberOfHMetrics;

  function OpenTypeFileHHEA() {
    $this->OpenTypeFileTable();
  }

  function _read($filehandle) {
    $content = fread($filehandle, 4+16*2);

    $unpacked = unpack("Nversion/nascender/ndescender/nlineGap/nadvanceWidthMax/nminLeftSideBearing/".
                       "nminRightSideBearing/nxMaxExtent/ncaretSlopeRise/ncaretSlopeRun/ncaretOffset/n4reserved/".
                       "nmetricDataFormat/nnumberOfHMetrics", $content);

    $version                    = $unpacked['version'];
    $this->_ascender            = $this->_fixFWord($unpacked['ascender']);
    $this->_descender           = $this->_fixFWord($unpacked['descender']);
    $this->_lineGap             = $this->_fixFWord($unpacked['lineGap']);
    $this->_advanceWidthMax     = $unpacked['advanceWidthMax']; 
    $this->_minLeftSideBearing  = $this->_fixFWord($unpacked['minLeftSideBearing']);
    $this->_minRightSideBearing = $this->_fixFWord($unpacked['minRightSideBearing']);
    $this->_xMaxExtent          = $this->_fixFWord($unpacked['xMaxExtent']);
    $this->_caretSlopeRise      = $this->_fixShort($unpacked['caretSlopeRise']);
    $this->_caretSlopeRun       = $this->_fixShort($unpacked['caretSlopeRun']);
    $this->_caretOffset         = $this->_fixShort($unpacked['caretOffset']);
    $this->_metricDataFormat    = $this->_fixShort($unpacked['metricDataFormat']);
    $this->_numberOfHMetrics    = $unpacked['numberOfHMetrics'];
  }
}

class OpenTypeFileHMTX extends OpenTypeFileTable {
  var $_hMetrics;
  var $_leftSideBearing;

  function _delete() {
    unset($this->_hMetrics);
    unset($this->_leftSideBearing);
  }

  function OpenTypeFileHMTX() {
    $this->OpenTypeFileTable();

    $this->_hMetrics        = array();
    $this->_leftSideBearing = array();
  }

  function _read($filehandle) {
    $fontFile =& $this->getFontFile();
    $hhea =& $fontFile->getTable('hhea');
    $maxp =& $fontFile->getTable('maxp');

    for ($i=0; $i<$hhea->_numberOfHMetrics; $i++) {
      $content = fread($filehandle, 2*2);
      $unpacked = unpack("nadvanceWidth/nlsb", $content);
      $this->_hMetrics[] = array('advanceWidth' => $unpacked['advanceWidth'],
                                 'lsb'          => $this->_fixShort($unpacked['lsb']));
    };

    for ($i=0; $i<$maxp->_numGlyphs; $i++) {
      $content = fread($filehandle, 2);
      $unpacked = unpack("nitem", $content);
      $this->_leftSideBearing[] = $unpacked['item'];
    };
  }
}


?>