<?php
/**
 * @package php-font-lib
 * @link    http://php-font-lib.googlecode.com/
 * @author  Fabien Mnager <fabien.menager@gmail.com>
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @version $Id: font_truetype.cls.php 41 2012-02-04 18:01:38Z fabien.menager $
 */

$dir = dirname(__FILE__);
require_once "$dir/font_binary_stream.cls.php";
require_once "$dir/font_truetype_table_directory_entry.cls.php";
require_once "$dir/font_truetype_header.cls.php";
require_once "$dir/font_table.cls.php";
require_once "$dir/adobe_font_metrics.cls.php";

/**
 * TrueType font file.
 * 
 * @package php-font-lib
 */
class Font_TrueType extends Font_Binary_Stream {
  /**
   * @var Font_TrueType_Header
   */
  public $header = array();
  
  private $tableOffset = 0; // Used for TTC
  
  private static $raw = false;
  
  protected $directory = array();
  protected $data = array();
  
  protected $glyph_subset = array();
  protected $glyph_all = array();
  public $compound_glyph_offsets = array();
  
  static $nameIdCodes = array(
    0  => "Copyright",
    1  => "FontName",
    2  => "FontSubfamily",
    3  => "UniqueID",
    4  => "FullName",
    5  => "Version",
    6  => "PostScriptName",
    7  => "Trademark",
    8  => "Manufacturer",
    9  => "Designer",
    10 => "Description",
    11 => "FontVendorURL",
    12 => "FontDesignerURL",
    13 => "LicenseDescription",
    14 => "LicenseURL",
 // 15
    16 => "PreferredFamily",
    17 => "PreferredSubfamily",
    18 => "CompatibleFullName",
    19 => "SampleText",
  );
  
  static $platforms = array(
    0 => "Unicode",
    1 => "Macintosh",
 // 2 =>  Reserved
    3 => "Microsoft",
  );
  
  static $plaformSpecific = array(
    // Unicode
    0 => array(
      0 => "Default semantics",
      1 => "Version 1.1 semantics",
      2 => "ISO 10646 1993 semantics (deprecated)",
      3 => "Unicode 2.0 or later semantics",
    ),
    
    // Macintosh
    1 => array(
      0 => "Roman",
      1 => "Japanese",
      2 => "Traditional Chinese",
      3 => "Korean",
      4 => "Arabic",  
      5 => "Hebrew",  
      6 => "Greek", 
      7 => "Russian", 
      8 => "RSymbol", 
      9 => "Devanagari",  
      10 => "Gurmukhi",  
      11 => "Gujarati",  
      12 => "Oriya", 
      13 => "Bengali", 
      14 => "Tamil", 
      15 => "Telugu",
      16 => "Kannada",
      17 => "Malayalam",
      18 => "Sinhalese",
      19 => "Burmese",
      20 => "Khmer",
      21 => "Thai",
      22 => "Laotian",
      23 => "Georgian",
      24 => "Armenian",
      25 => "Simplified Chinese",
      26 => "Tibetan",
      27 => "Mongolian",
      28 => "Geez",
      29 => "Slavic",
      30 => "Vietnamese",
      31 => "Sindhi",
    ),
    
    // Microsoft
    3 => array(
      0 => "Symbol",
      1 => "Unicode BMP (UCS-2)",
      2 => "ShiftJIS",
      3 => "PRC",
      4 => "Big5",
      5 => "Wansung",
      6 => "Johab",
  //  7 => Reserved
  //  8 => Reserved
  //  9 => Reserved
      10 => "Unicode UCS-4",
    ),
  );
  
  static $macCharNames = array(
    ".notdef", ".null", "CR",
    "space", "exclam", "quotedbl", "numbersign",
    "dollar", "percent", "ampersand", "quotesingle",
    "parenleft", "parenright", "asterisk", "plus",
    "comma", "hyphen", "period", "slash",
    "zero", "one", "two", "three",
    "four", "five", "six", "seven",
    "eight", "nine", "colon", "semicolon",
    "less", "equal", "greater", "question",
    "at", "A", "B", "C", "D", "E", "F", "G",
    "H", "I", "J", "K", "L", "M", "N", "O",
    "P", "Q", "R", "S", "T", "U", "V", "W",
    "X", "Y", "Z", "bracketleft",
    "backslash", "bracketright", "asciicircum", "underscore",
    "grave", "a", "b", "c", "d", "e", "f", "g",
    "h", "i", "j", "k", "l", "m", "n", "o",
    "p", "q", "r", "s", "t", "u", "v", "w",
    "x", "y", "z", "braceleft",
    "bar", "braceright", "asciitilde", "Adieresis",
    "Aring", "Ccedilla", "Eacute", "Ntilde",
    "Odieresis", "Udieresis", "aacute", "agrave",
    "acircumflex", "adieresis", "atilde", "aring",
    "ccedilla", "eacute", "egrave", "ecircumflex",
    "edieresis", "iacute", "igrave", "icircumflex",
    "idieresis", "ntilde", "oacute", "ograve",
    "ocircumflex", "odieresis", "otilde", "uacute",
    "ugrave", "ucircumflex", "udieresis", "dagger",
    "degree", "cent", "sterling", "section",
    "bullet", "paragraph", "germandbls", "registered",
    "copyright", "trademark", "acute", "dieresis",
    "notequal", "AE", "Oslash", "infinity",
    "plusminus", "lessequal", "greaterequal", "yen",
    "mu", "partialdiff", "summation", "product",
    "pi", "integral", "ordfeminine", "ordmasculine",
    "Omega", "ae", "oslash", "questiondown",
    "exclamdown", "logicalnot", "radical", "florin",
    "approxequal", "increment", "guillemotleft", "guillemotright",
    "ellipsis", "nbspace", "Agrave", "Atilde",
    "Otilde", "OE", "oe", "endash",
    "emdash", "quotedblleft", "quotedblright", "quoteleft",
    "quoteright", "divide", "lozenge", "ydieresis",
    "Ydieresis", "fraction", "currency", "guilsinglleft",
    "guilsinglright", "fi", "fl", "daggerdbl",
    "periodcentered", "quotesinglbase", "quotedblbase", "perthousand",
    "Acircumflex", "Ecircumflex", "Aacute", "Edieresis",
    "Egrave", "Iacute", "Icircumflex", "Idieresis",
    "Igrave", "Oacute", "Ocircumflex", "applelogo",
    "Ograve", "Uacute", "Ucircumflex", "Ugrave",
    "dotlessi", "circumflex", "tilde", "macron",
    "breve", "dotaccent", "ring", "cedilla",
    "hungarumlaut", "ogonek", "caron", "Lslash",
    "lslash", "Scaron", "scaron", "Zcaron",
    "zcaron", "brokenbar", "Eth", "eth",
    "Yacute", "yacute", "Thorn", "thorn",
    "minus", "multiply", "onesuperior", "twosuperior",
    "threesuperior", "onehalf", "onequarter", "threequarters",
    "franc", "Gbreve", "gbreve", "Idot",
    "Scedilla", "scedilla", "Cacute", "cacute",
    "Ccaron", "ccaron", "dmacron"
  );
  
  function getTable(){
    $this->parseTableEntries();
    return $this->directory;
  }
  
  function setTableOffset($offset) {
    $this->tableOffset = $offset;
  }
  
  function parse() {
    $this->parseTableEntries();
    
    $this->data = array();
    
    foreach($this->directory as $tag => $table) {
      if (empty($this->data[$tag])) {
        $this->readTable($tag);
      }
    }
  }
  
  function utf8toUnicode($str) {
    $len = strlen($str);
    $out = array();

    for ($i = 0; $i < $len; $i++) {
      $uni = -1;
      $h = ord($str[$i]);
      
      if ( $h <= 0x7F ) {
        $uni = $h;
      }
      elseif ( $h >= 0xC2 ) {
        if ( ($h <= 0xDF) && ($i < $len -1) )
          $uni = ($h & 0x1F) << 6 | (ord($str[++$i]) & 0x3F);
        elseif ( ($h <= 0xEF) && ($i < $len -2) )
          $uni = ($h & 0x0F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F);
        elseif ( ($h <= 0xF4) && ($i < $len -3) )
          $uni = ($h & 0x0F) << 18 | (ord($str[++$i]) & 0x3F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F);
      }
      
      if ($uni >= 0) {
        $out[] = $uni;
      }
    }
    
    return $out;
  }
  
  function lookupGlyph($gid, &$gids, &$newGlyphOffsets, $glyfOffset, $indexToLoc, $gidToCid) {
    $this->seek($glyfOffset + $indexToLoc[$gid]);
    
    $numberOfContours = $this->readInt16();
    
    if ($numberOfContours < 0) {
      $this->skip(8);
      $compoundOffset = 10; // 2 + 8
      
      do {
        $flags      = $this->readUInt16();
        $compoundOffset += 2;
        
        $glyphIndex = $this->readUInt16();
        $newGlyphOffsets[$compoundOffset] = $glyphIndex;
        
        $compoundOffset += 2;
        
        if (!in_array($glyphIndex, $gids) && isset($indexToLoc[$glyphIndex])) {
          $code = $gidToCid[$glyphIndex];
          $gids[$code] = $glyphIndex;
        }
        
        $pos = $this->pos();
        $this->lookupGlyph($glyphIndex, $gids, $newGids, $glyfOffset, $indexToLoc, $gidToCid);
        $this->seek($pos);
        
        $offset = 0;
        
        // skip some bytes by case
        if ($flags & Font_Table_glyf::ARG_1_AND_2_ARE_WORDS) {
          $offset += 4;
        }
        else {
          $offset += 2;
        }
        
        if ($flags & Font_Table_glyf::WE_HAVE_A_SCALE) {
          $offset += 2;
        }
        elseif ($flags & Font_Table_glyf::WE_HAVE_AN_X_AND_Y_SCALE) {
          $offset += 4;
        }
        elseif ($flags & Font_Table_glyf::WE_HAVE_A_TWO_BY_TWO) {
          $offset += 8;
        }
        
        $this->skip($offset);
        $compoundOffset += $offset;
        
      } while ($flags & Font_Table_glyf::MORE_COMPONENTS);
    }
  }
  
  function setSubset($subset) {
    if ( !is_array($subset) ) {
      $subset = $this->utf8toUnicode($subset);
    }
    
    $subtable = null;
    foreach($this->getData("cmap", "subtables") as $_subtable) {
      if ($_subtable["platformID"] == 0 || $_subtable["platformID"] == 3 && $_subtable["platformSpecificID"] == 1) {
        $subtable = $_subtable;
        break;
      }
    }
    
    if (!$subtable) return;
    
    $gids = array();
    foreach($subset as $code) {
      if (!isset($subtable["glyphIndexArray"][$code])) {
        continue;
      }
      
      $gids[$code] = $subtable["glyphIndexArray"][$code];
    }
    
    // add compound glyphs
    $indexToLoc = $this->getData("loca");
    $glyfOffset = $this->directory["glyf"]->offset;
    $cidToGid   = $subtable["glyphIndexArray"];
    $gidToCid   = array_flip($cidToGid);
    $newGlyphOffsets = array();
    
    foreach($gids as $code => $gid) {
      if ($gid === null) {
        unset($gids[$code]);
        continue;
      }
      
      $_newGlyphOffsets = array();
      $this->lookupGlyph($gid, $gids, $_newGlyphOffsets, $glyfOffset, $indexToLoc, $gidToCid);
      
      if (count($_newGlyphOffsets)) {
        $newGlyphOffsets[$gid] = $_newGlyphOffsets;
      }
    }
    
    ksort($gids);
    
    foreach($newGlyphOffsets as $_gid => $compoundOffsets) {
      foreach($compoundOffsets as $offset => $gid) {
        $newGlyphOffsets[$_gid][$offset] = array_search($gid, array_values($gids));
      }
    }
    
    $this->compound_glyph_offsets = $newGlyphOffsets;
    $this->glyph_subset = $gids;
    $this->glyph_all = $subtable["glyphIndexArray"];
  }
  
  function getSubset() {
    if (empty($this->glyph_subset)) {
      return $this->glyph_all;
    }
    
    return $this->glyph_subset;
  }
  
  function encode($tags = array()){
    if (!self::$raw) {
      $tags = array_merge(array("head", "hhea", "cmap", "hmtx", "maxp", "glyf", "loca", "name", "post"), $tags);
    }
    else {
      $tags = array_keys($this->directory);
    }
    
    $num_tables = count($tags);
    $n = 16;// @todo
    
    Font::d("Tables : ".implode(", ", $tags));
    
    $entries = array();
    foreach($tags as $tag) {
      if (!isset($this->directory[$tag])) {
        Font::d("  >> '$tag' table doesn't exist");
        continue;
      }
      
      $entries[$tag] = $this->directory[$tag];
    }
    
    $this->header->data["numTables"] = $num_tables;
    $this->header->encode();
    
    $directory_offset = $this->pos();
    $offset = $directory_offset + $num_tables * $n;
    $this->seek($offset);
    
    $i = 0;
    foreach($entries as $tag => $entry) {
      $entry->encode($directory_offset + $i * $n);
      $i++;
    }
  }
  
  function parseHeader(){
    if (!empty($this->header)) {
      return;
    }
    
    $this->seek($this->tableOffset);
    
    $this->header = new Font_TrueType_Header($this);
    $this->header->parse();
  }
  
  function parseTableEntries(){
    $this->parseHeader();
    
    if (!empty($this->directory)) {
      return;
    }
    
    $class = get_class($this)."_Table_Directory_Entry";
    
    for($i = 0; $i < $this->header->data["numTables"]; $i++) {
      $entry = new $class($this);
      $this->directory[$entry->tag] = $entry;
    }
  }
  
  function normalizeFUnit($value, $base = 1000){
    return round($value * ($base / $this->getData("head", "unitsPerEm")));
  }
  
  protected function readTable($tag) {
    $this->parseTableEntries();
    
    if (!self::$raw) {
      $name_canon = preg_replace("/[^a-z0-9]/", "", strtolower($tag));
      $class_file = dirname(__FILE__)."/font_table_$name_canon.cls.php";
      
      if (!isset($this->directory[$tag]) || !file_exists($class_file)) {
        return;
      }
      
      require_once $class_file;
      $class = "Font_Table_$name_canon";
    }
    else {
      $class = "Font_Table";
    }
    
    $table = new $class($this->directory[$tag]);
    $table->parse();
    
    $this->data[$tag] = $table;
  }
  
  public function getTableObject($name) {
    return $this->data[$name];
  }
  
  public function getData($name, $key = null) {
    $this->parseTableEntries();
    
    if (empty($this->data[$name])) {
      $this->readTable($name);
    }
    
    if (!isset($this->data[$name])) {
      return null;
    }
    
    if (!$key) {
      return $this->data[$name]->data;
    }
    else {
      return $this->data[$name]->data[$key];
    }
  }
  
  function saveAdobeFontMetrics($file, $encoding = null) {
    $afm = new Adobe_Font_Metrics($this);
    $afm->write($file, $encoding);
  }
  
  function reduce(){
    $names_to_keep = array(0, 1, 2, 3, 4, 5, 6);
    foreach($this->data["name"]->data["records"] as $id => $rec) {
      if (in_array($id, $names_to_keep)) continue;
      unset($this->data["name"]->data["records"][$id]);
    }
  }
}
