<?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'];
 
    }
 
}
 
?>
 
 
 |