Subversion-Projekte lars-tiefland.php_share

Revision

Blame | Letzte Änderung | Log anzeigen | RSS feed

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