<?php
/*******************************************************************************
* Utility to parse TTF font files                                              *
*                                                                              *
* Version: 1.0                                                                 *
* Date:    2011-06-18                                                          *
* Author:  Olivier PLATHEY                                                     *
*******************************************************************************/

class TTFParser
{
    var $f;
    var $tables;
    var $unitsPerEm;
    var $xMin, $yMin, $xMax, $yMax;
    var $numberOfHMetrics;
    var $numGlyphs;
    var $widths;
    var $chars;
    var $postScriptName;
    var $Embeddable;
    var $Bold;
    var $typoAscender;
    var $typoDescender;
    var $capHeight;
    var $italicAngle;
    var $underlinePosition;
    var $underlineThickness;
    var $isFixedPitch;

    function Parse($file)
    {
        $this->f = fopen($file, 'rb');
        if(!$this->f)
            $this->Error('Can\'t open file: '.$file);

        $version = $this->Read(4);
        if($version=='OTTO')
            $this->Error('OpenType fonts based on PostScript outlines are not supported');
        if($version!="\x00\x01\x00\x00")
            $this->Error('Unrecognized file format');
        $numTables = $this->ReadUShort();
        $this->Skip(3*2); // searchRange, entrySelector, rangeShift
        $this->tables = array();
        for($i=0;$i<$numTables;$i++)
        {
            $tag = $this->Read(4);
            $this->Skip(4); // checkSum
            $offset = $this->ReadULong();
            $this->Skip(4); // length
            $this->tables[$tag] = $offset;
        }

        $this->ParseHead();
        $this->ParseHhea();
        $this->ParseMaxp();
        $this->ParseHmtx();
        $this->ParseCmap();
        $this->ParseName();
        $this->ParseOS2();
        $this->ParsePost();

        fclose($this->f);
    }

    function ParseHead()
    {
        $this->Seek('head');
        $this->Skip(3*4); // version, fontRevision, checkSumAdjustment
        $magicNumber = $this->ReadULong();
        if($magicNumber!=0x5F0F3CF5)
            $this->Error('Incorrect magic number');
        $this->Skip(2); // flags
        $this->unitsPerEm = $this->ReadUShort();
        $this->Skip(2*8); // created, modified
        $this->xMin = $this->ReadShort();
        $this->yMin = $this->ReadShort();
        $this->xMax = $this->ReadShort();
        $this->yMax = $this->ReadShort();
    }

    function ParseHhea()
    {
        $this->Seek('hhea');
        $this->Skip(4+15*2);
        $this->numberOfHMetrics = $this->ReadUShort();
    }

    function ParseMaxp()
    {
        $this->Seek('maxp');
        $this->Skip(4);
        $this->numGlyphs = $this->ReadUShort();
    }

    function ParseHmtx()
    {
        $this->Seek('hmtx');
        $this->widths = array();
        for($i=0;$i<$this->numberOfHMetrics;$i++)
        {
            $advanceWidth = $this->ReadUShort();
            $this->Skip(2); // lsb
            $this->widths[$i] = $advanceWidth;
        }
        if($this->numberOfHMetrics<$this->numGlyphs)
        {
            $lastWidth = $this->widths[$this->numberOfHMetrics-1];
            $this->widths = array_pad($this->widths, $this->numGlyphs, $lastWidth);
        }
    }

    function ParseCmap()
    {
        $this->Seek('cmap');
        $this->Skip(2); // version
        $numTables = $this->ReadUShort();
        $offset31 = 0;
        for($i=0;$i<$numTables;$i++)
        {
            $platformID = $this->ReadUShort();
            $encodingID = $this->ReadUShort();
            $offset = $this->ReadULong();
            if($platformID==3 && $encodingID==1)
                $offset31 = $offset;
        }
        if($offset31==0)
            $this->Error('No Unicode encoding found');

        $startCount = array();
        $endCount = array();
        $idDelta = array();
        $idRangeOffset = array();
        $this->chars = array();
        fseek($this->f, $this->tables['cmap']+$offset31, SEEK_SET);
        $format = $this->ReadUShort();
        if($format!=4)
            $this->Error('Unexpected subtable format: '.$format);
        $this->Skip(2*2); // length, language
        $segCount = $this->ReadUShort()/2;
        $this->Skip(3*2); // searchRange, entrySelector, rangeShift
        for($i=0;$i<$segCount;$i++)
            $endCount[$i] = $this->ReadUShort();
        $this->Skip(2); // reservedPad
        for($i=0;$i<$segCount;$i++)
            $startCount[$i] = $this->ReadUShort();
        for($i=0;$i<$segCount;$i++)
            $idDelta[$i] = $this->ReadShort();
        $offset = ftell($this->f);
        for($i=0;$i<$segCount;$i++)
            $idRangeOffset[$i] = $this->ReadUShort();

        for($i=0;$i<$segCount;$i++)
        {
            $c1 = $startCount[$i];
            $c2 = $endCount[$i];
            $d = $idDelta[$i];
            $ro = $idRangeOffset[$i];
            if($ro>0)
                fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
            for($c=$c1;$c<=$c2;$c++)
            {
                if($c==0xFFFF)
                    break;
                if($ro>0)
                {
                    $gid = $this->ReadUShort();
                    if($gid>0)
                        $gid += $d;
                }
                else
                    $gid = $c+$d;
                if($gid>=65536)
                    $gid -= 65536;
                if($gid>0)
                    $this->chars[$c] = $gid;
            }
        }
    }

    function ParseName()
    {
        $this->Seek('name');
        $tableOffset = ftell($this->f);
        $this->postScriptName = '';
        $this->Skip(2); // format
        $count = $this->ReadUShort();
        $stringOffset = $this->ReadUShort();
        for($i=0;$i<$count;$i++)
        {
            $this->Skip(3*2); // platformID, encodingID, languageID
            $nameID = $this->ReadUShort();
            $length = $this->ReadUShort();
            $offset = $this->ReadUShort();
            if($nameID==6)
            {
                // PostScript name
                fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
                $s = $this->Read($length);
                $s = str_replace(chr(0), '', $s);
                $s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
                $this->postScriptName = $s;
                break;
            }
        }
        if($this->postScriptName=='')
            $this->Error('PostScript name not found');
    }

    function ParseOS2()
    {
        $this->Seek('OS/2');
        $version = $this->ReadUShort();
        $this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
        $fsType = $this->ReadUShort();
        $this->Embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
        $this->Skip(11*2+10+4*4+4);
        $fsSelection = $this->ReadUShort();
        $this->Bold = ($fsSelection & 32)!=0;
        $this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
        $this->typoAscender = $this->ReadShort();
        $this->typoDescender = $this->ReadShort();
        if($version>=2)
        {
            $this->Skip(3*2+2*4+2);
            $this->capHeight = $this->ReadShort();
        }
        else
            $this->capHeight = 0;
    }

    function ParsePost()
    {
        $this->Seek('post');
        $this->Skip(4); // version
        $this->italicAngle = $this->ReadShort();
        $this->Skip(2); // Skip decimal part
        $this->underlinePosition = $this->ReadShort();
        $this->underlineThickness = $this->ReadShort();
        $this->isFixedPitch = ($this->ReadULong()!=0);
    }

    function Error($msg)
    {
        if(PHP_SAPI=='cli')
            die("Error: $msg\n");
        else
            die("<b>Error</b>: $msg");
    }

    function Seek($tag)
    {
        if(!isset($this->tables[$tag]))
            $this->Error('Table not found: '.$tag);
        fseek($this->f, $this->tables[$tag], SEEK_SET);
    }

    function Skip($n)
    {
        fseek($this->f, $n, SEEK_CUR);
    }

    function Read($n)
    {
        return fread($this->f, $n);
    }

    function ReadUShort()
    {
        $a = unpack('nn', fread($this->f,2));
        return $a['n'];
    }

    function ReadShort()
    {
        $a = unpack('nn', fread($this->f,2));
        $v = $a['n'];
        if($v>=0x8000)
            $v -= 65536;
        return $v;
    }

    function ReadULong()
    {
        $a = unpack('NN', fread($this->f,4));
        return $a['N'];
    }
}
