AutorÃa | Ultima modificación | Ver Log |
<?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 formatfor($i=0;$i<=$this->numGlyphs;$i++)$offsets[] = 2*$this->ReadUShort();}else{// Long formatfor($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, rangeShiftfor($i=0;$i<$segCount;$i++)$endCount[$i] = $this->ReadUShort();$this->Skip(2); // reservedPadfor($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 namefseek($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 charif($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); // reservedPadforeach($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 glyphforeach($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);}}?>