123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- <?php
- /*******************************************************************************
- * Class to parse and subset TrueType fonts *
- * *
- * Version: 1.1 *
- * Date: 2015-11-29 *
- * Author: Olivier PLATHEY *
- *******************************************************************************/
- class TTFParser
- {
- protected $f;
- protected $tables;
- protected $numberOfHMetrics;
- protected $numGlyphs;
- protected $glyphNames;
- protected $indexToLocFormat;
- protected $subsettedChars;
- protected $subsettedGlyphs;
- public $chars;
- public $glyphs;
- public $unitsPerEm;
- public $xMin, $yMin, $xMax, $yMax;
- public $postScriptName;
- public $embeddable;
- public $bold;
- public $typoAscender;
- public $typoDescender;
- public $capHeight;
- public $italicAngle;
- public $underlinePosition;
- public $underlineThickness;
- public $isFixedPitch;
- function __construct($file)
- {
- $this->f = fopen($file, 'rb');
- if(!$this->f)
- $this->Error('Can\'t open file: '.$file);
- }
- function __destruct()
- {
- if(is_resource($this->f))
- fclose($this->f);
- }
- function Parse()
- {
- $this->ParseOffsetTable();
- $this->ParseHead();
- $this->ParseHhea();
- $this->ParseMaxp();
- $this->ParseHmtx();
- $this->ParseLoca();
- $this->ParseGlyf();
- $this->ParseCmap();
- $this->ParseName();
- $this->ParseOS2();
- $this->ParsePost();
- }
- function ParseOffsetTable()
- {
- $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);
- $checkSum = $this->Read(4);
- $offset = $this->ReadULong();
- $length = $this->ReadULong(4);
- $this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum);
- }
- }
- 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();
- $this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint
- $this->indexToLocFormat = $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->glyphs = array();
- for($i=0;$i<$this->numberOfHMetrics;$i++)
- {
- $advanceWidth = $this->ReadUShort();
- $lsb = $this->ReadShort();
- $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
- }
- for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++)
- {
- $lsb = $this->ReadShort();
- $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
- }
- }
- function ParseLoca()
- {
- $this->Seek('loca');
- $offsets = array();
- if($this->indexToLocFormat==0)
- {
- // Short format
- for($i=0;$i<=$this->numGlyphs;$i++)
- $offsets[] = 2*$this->ReadUShort();
- }
- else
- {
- // Long format
- for($i=0;$i<=$this->numGlyphs;$i++)
- $offsets[] = $this->ReadULong();
- }
- for($i=0;$i<$this->numGlyphs;$i++)
- {
- $this->glyphs[$i]['offset'] = $offsets[$i];
- $this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i];
- }
- }
- function ParseGlyf()
- {
- $tableOffset = $this->tables['glyf']['offset'];
- foreach($this->glyphs as &$glyph)
- {
- if($glyph['length']>0)
- {
- fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
- if($this->ReadShort()<0)
- {
- // Composite glyph
- $this->Skip(4*2); // xMin, yMin, xMax, yMax
- $offset = 5*2;
- $a = array();
- do
- {
- $flags = $this->ReadUShort();
- $index = $this->ReadUShort();
- $a[$offset+2] = $index;
- if($flags & 1) // ARG_1_AND_2_ARE_WORDS
- $skip = 2*2;
- else
- $skip = 2;
- if($flags & 8) // WE_HAVE_A_SCALE
- $skip += 2;
- elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE
- $skip += 2*2;
- elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO
- $skip += 4*2;
- $this->Skip($skip);
- $offset += 2*2 + $skip;
- }
- while($flags & 32); // MORE_COMPONENTS
- $glyph['components'] = $a;
- }
- }
- }
- }
- 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']['offset']+$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 = $this->tables['name']['offset'];
- $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');
- $version = $this->ReadULong();
- $this->italicAngle = $this->ReadShort();
- $this->Skip(2); // Skip decimal part
- $this->underlinePosition = $this->ReadShort();
- $this->underlineThickness = $this->ReadShort();
- $this->isFixedPitch = ($this->ReadULong()!=0);
- if($version==0x20000)
- {
- // Extract glyph names
- $this->Skip(4*4); // min/max usage
- $this->Skip(2); // numberOfGlyphs
- $glyphNameIndex = array();
- $names = array();
- $numNames = 0;
- for($i=0;$i<$this->numGlyphs;$i++)
- {
- $index = $this->ReadUShort();
- $glyphNameIndex[] = $index;
- if($index>=258 && $index-257>$numNames)
- $numNames = $index-257;
- }
- for($i=0;$i<$numNames;$i++)
- {
- $len = ord($this->Read(1));
- $names[] = $this->Read($len);
- }
- foreach($glyphNameIndex as $i=>$index)
- {
- if($index>=258)
- $this->glyphs[$i]['name'] = $names[$index-258];
- else
- $this->glyphs[$i]['name'] = $index;
- }
- $this->glyphNames = true;
- }
- else
- $this->glyphNames = false;
- }
- function Subset($chars)
- {
- /* $chars = array_keys($this->chars);
- $this->subsettedChars = $chars;
- $this->subsettedGlyphs = array();
- for($i=0;$i<$this->numGlyphs;$i++)
- {
- $this->subsettedGlyphs[] = $i;
- $this->glyphs[$i]['ssid'] = $i;
- }*/
- $this->AddGlyph(0);
- $this->subsettedChars = array();
- foreach($chars as $char)
- {
- if(isset($this->chars[$char]))
- {
- $this->subsettedChars[] = $char;
- $this->AddGlyph($this->chars[$char]);
- }
- }
- }
- function AddGlyph($id)
- {
- if(!isset($this->glyphs[$id]['ssid']))
- {
- $this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs);
- $this->subsettedGlyphs[] = $id;
- if(isset($this->glyphs[$id]['components']))
- {
- foreach($this->glyphs[$id]['components'] as $cid)
- $this->AddGlyph($cid);
- }
- }
- }
- function Build()
- {
- $this->BuildCmap();
- $this->BuildHhea();
- $this->BuildHmtx();
- $this->BuildLoca();
- $this->BuildGlyf();
- $this->BuildMaxp();
- $this->BuildPost();
- return $this->BuildFont();
- }
- function BuildCmap()
- {
- if(!isset($this->subsettedChars))
- return;
- // Divide charset in contiguous segments
- $chars = $this->subsettedChars;
- sort($chars);
- $segments = array();
- $segment = array($chars[0], $chars[0]);
- for($i=1;$i<count($chars);$i++)
- {
- if($chars[$i]>$segment[1]+1)
- {
- $segments[] = $segment;
- $segment = array($chars[$i], $chars[$i]);
- }
- else
- $segment[1]++;
- }
- $segments[] = $segment;
- $segments[] = array(0xFFFF, 0xFFFF);
- $segCount = count($segments);
- // Build a Format 4 subtable
- $startCount = array();
- $endCount = array();
- $idDelta = array();
- $idRangeOffset = array();
- $glyphIdArray = '';
- for($i=0;$i<$segCount;$i++)
- {
- list($start, $end) = $segments[$i];
- $startCount[] = $start;
- $endCount[] = $end;
- if($start!=$end)
- {
- // Segment with multiple chars
- $idDelta[] = 0;
- $idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2;
- for($c=$start;$c<=$end;$c++)
- {
- $ssid = $this->glyphs[$this->chars[$c]]['ssid'];
- $glyphIdArray .= pack('n', $ssid);
- }
- }
- else
- {
- // Segment with a single char
- if($start<0xFFFF)
- $ssid = $this->glyphs[$this->chars[$start]]['ssid'];
- else
- $ssid = 0;
- $idDelta[] = $ssid - $start;
- $idRangeOffset[] = 0;
- }
- }
- $entrySelector = 0;
- $n = $segCount;
- while($n!=1)
- {
- $n = $n>>1;
- $entrySelector++;
- }
- $searchRange = (1<<$entrySelector)*2;
- $rangeShift = 2*$segCount - $searchRange;
- $cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift);
- foreach($endCount as $val)
- $cmap .= pack('n', $val);
- $cmap .= pack('n', 0); // reservedPad
- foreach($startCount as $val)
- $cmap .= pack('n', $val);
- foreach($idDelta as $val)
- $cmap .= pack('n', $val);
- foreach($idRangeOffset as $val)
- $cmap .= pack('n', $val);
- $cmap .= $glyphIdArray;
- $data = pack('nn', 0, 1); // version, numTables
- $data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset
- $data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language
- $data .= $cmap;
- $this->SetTable('cmap', $data);
- }
- function BuildHhea()
- {
- $this->LoadTable('hhea');
- $numberOfHMetrics = count($this->subsettedGlyphs);
- $data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2);
- $this->SetTable('hhea', $data);
- }
- function BuildHmtx()
- {
- $data = '';
- foreach($this->subsettedGlyphs as $id)
- {
- $glyph = $this->glyphs[$id];
- $data .= pack('nn', $glyph['w'], $glyph['lsb']);
- }
- $this->SetTable('hmtx', $data);
- }
- function BuildLoca()
- {
- $data = '';
- $offset = 0;
- foreach($this->subsettedGlyphs as $id)
- {
- if($this->indexToLocFormat==0)
- $data .= pack('n', $offset/2);
- else
- $data .= pack('N', $offset);
- $offset += $this->glyphs[$id]['length'];
- }
- if($this->indexToLocFormat==0)
- $data .= pack('n', $offset/2);
- else
- $data .= pack('N', $offset);
- $this->SetTable('loca', $data);
- }
- function BuildGlyf()
- {
- $tableOffset = $this->tables['glyf']['offset'];
- $data = '';
- foreach($this->subsettedGlyphs as $id)
- {
- $glyph = $this->glyphs[$id];
- fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
- $glyph_data = $this->Read($glyph['length']);
- if(isset($glyph['components']))
- {
- // Composite glyph
- foreach($glyph['components'] as $offset=>$cid)
- {
- $ssid = $this->glyphs[$cid]['ssid'];
- $glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2);
- }
- }
- $data .= $glyph_data;
- }
- $this->SetTable('glyf', $data);
- }
- function BuildMaxp()
- {
- $this->LoadTable('maxp');
- $numGlyphs = count($this->subsettedGlyphs);
- $data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2);
- $this->SetTable('maxp', $data);
- }
- function BuildPost()
- {
- $this->Seek('post');
- if($this->glyphNames)
- {
- // Version 2.0
- $numberOfGlyphs = count($this->subsettedGlyphs);
- $numNames = 0;
- $names = '';
- $data = $this->Read(2*4+2*2+5*4);
- $data .= pack('n', $numberOfGlyphs);
- foreach($this->subsettedGlyphs as $id)
- {
- $name = $this->glyphs[$id]['name'];
- if(is_string($name))
- {
- $data .= pack('n', 258+$numNames);
- $names .= chr(strlen($name)).$name;
- $numNames++;
- }
- else
- $data .= pack('n', $name);
- }
- $data .= $names;
- }
- else
- {
- // Version 3.0
- $this->Skip(4);
- $data = "\x00\x03\x00\x00";
- $data .= $this->Read(4+2*2+5*4);
- }
- $this->SetTable('post', $data);
- }
- function BuildFont()
- {
- $tags = array();
- foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag)
- {
- if(isset($this->tables[$tag]))
- $tags[] = $tag;
- }
- $numTables = count($tags);
- $offset = 12 + 16*$numTables;
- foreach($tags as $tag)
- {
- if(!isset($this->tables[$tag]['data']))
- $this->LoadTable($tag);
- $this->tables[$tag]['offset'] = $offset;
- $offset += strlen($this->tables[$tag]['data']);
- }
- // $this->tables['head']['data'] = substr_replace($this->tables['head']['data'], "\x00\x00\x00\x00", 8, 4);
- // Build offset table
- $entrySelector = 0;
- $n = $numTables;
- while($n!=1)
- {
- $n = $n>>1;
- $entrySelector++;
- }
- $searchRange = 16*(1<<$entrySelector);
- $rangeShift = 16*$numTables - $searchRange;
- $offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift);
- foreach($tags as $tag)
- {
- $table = $this->tables[$tag];
- $offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']);
- }
- // Compute checkSumAdjustment (0xB1B0AFBA - font checkSum)
- $s = $this->CheckSum($offsetTable);
- foreach($tags as $tag)
- $s .= $this->tables[$tag]['checkSum'];
- $a = unpack('n2', $this->CheckSum($s));
- $high = 0xB1B0 + ($a[1]^0xFFFF);
- $low = 0xAFBA + ($a[2]^0xFFFF) + 1;
- $checkSumAdjustment = pack('nn', $high+($low>>16), $low);
- $this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4);
- $font = $offsetTable;
- foreach($tags as $tag)
- $font .= $this->tables[$tag]['data'];
- return $font;
- }
- function LoadTable($tag)
- {
- $this->Seek($tag);
- $length = $this->tables[$tag]['length'];
- $n = $length % 4;
- if($n>0)
- $length += 4 - $n;
- $this->tables[$tag]['data'] = $this->Read($length);
- }
- function SetTable($tag, $data)
- {
- $length = strlen($data);
- $n = $length % 4;
- if($n>0)
- $data = str_pad($data, $length+4-$n, "\x00");
- $this->tables[$tag]['data'] = $data;
- $this->tables[$tag]['length'] = $length;
- $this->tables[$tag]['checkSum'] = $this->CheckSum($data);
- }
- function Seek($tag)
- {
- if(!isset($this->tables[$tag]))
- $this->Error('Table not found: '.$tag);
- fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET);
- }
- function Skip($n)
- {
- fseek($this->f, $n, SEEK_CUR);
- }
- function Read($n)
- {
- return $n>0 ? 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'];
- }
- function CheckSum($s)
- {
- $n = strlen($s);
- $high = 0;
- $low = 0;
- for($i=0;$i<$n;$i+=4)
- {
- $high += (ord($s[$i])<<8) + ord($s[$i+1]);
- $low += (ord($s[$i+2])<<8) + ord($s[$i+3]);
- }
- return pack('nn', $high+($low>>16), $low);
- }
- function Error($msg)
- {
- throw new Exception($msg);
- }
- }
- ?>
|