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$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); } } ?>