COSC4606-Assignment-02

Database front end that allows for CRUD operations and user management
git clone git://mattcarlson.org/repos/COSC4606-Assignment-02.git
Log | Files | Refs | README

ttfparser.php (17951B)


      1 <?php
      2 /*******************************************************************************
      3 * Class to parse and subset TrueType fonts                                     *
      4 *                                                                              *
      5 * Version: 1.11                                                                *
      6 * Date:    2021-04-18                                                          *
      7 * Author:  Olivier PLATHEY                                                     *
      8 *******************************************************************************/
      9 
     10 class TTFParser
     11 {
     12 	protected $f;
     13 	protected $tables;
     14 	protected $numberOfHMetrics;
     15 	protected $numGlyphs;
     16 	protected $glyphNames;
     17 	protected $indexToLocFormat;
     18 	protected $subsettedChars;
     19 	protected $subsettedGlyphs;
     20 	public $chars;
     21 	public $glyphs;
     22 	public $unitsPerEm;
     23 	public $xMin, $yMin, $xMax, $yMax;
     24 	public $postScriptName;
     25 	public $embeddable;
     26 	public $bold;
     27 	public $typoAscender;
     28 	public $typoDescender;
     29 	public $capHeight;
     30 	public $italicAngle;
     31 	public $underlinePosition;
     32 	public $underlineThickness;
     33 	public $isFixedPitch;
     34 
     35 	function __construct($file)
     36 	{
     37 		$this->f = fopen($file, 'rb');
     38 		if(!$this->f)
     39 			$this->Error('Can\'t open file: '.$file);
     40 	}
     41 
     42 	function __destruct()
     43 	{
     44 		if(is_resource($this->f))
     45 			fclose($this->f);
     46 	}
     47 
     48 	function Parse()
     49 	{
     50 		$this->ParseOffsetTable();
     51 		$this->ParseHead();
     52 		$this->ParseHhea();
     53 		$this->ParseMaxp();
     54 		$this->ParseHmtx();
     55 		$this->ParseLoca();
     56 		$this->ParseGlyf();
     57 		$this->ParseCmap();
     58 		$this->ParseName();
     59 		$this->ParseOS2();
     60 		$this->ParsePost();
     61 	}
     62 
     63 	function ParseOffsetTable()
     64 	{
     65 		$version = $this->Read(4);
     66 		if($version=='OTTO')
     67 			$this->Error('OpenType fonts based on PostScript outlines are not supported');
     68 		if($version!="\x00\x01\x00\x00")
     69 			$this->Error('Unrecognized file format');
     70 		$numTables = $this->ReadUShort();
     71 		$this->Skip(3*2); // searchRange, entrySelector, rangeShift
     72 		$this->tables = array();
     73 		for($i=0;$i<$numTables;$i++)
     74 		{
     75 			$tag = $this->Read(4);
     76 			$checkSum = $this->Read(4);
     77 			$offset = $this->ReadULong();
     78 			$length = $this->ReadULong();
     79 			$this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum);
     80 		}
     81 	}	
     82 
     83 	function ParseHead()
     84 	{
     85 		$this->Seek('head');
     86 		$this->Skip(3*4); // version, fontRevision, checkSumAdjustment
     87 		$magicNumber = $this->ReadULong();
     88 		if($magicNumber!=0x5F0F3CF5)
     89 			$this->Error('Incorrect magic number');
     90 		$this->Skip(2); // flags
     91 		$this->unitsPerEm = $this->ReadUShort();
     92 		$this->Skip(2*8); // created, modified
     93 		$this->xMin = $this->ReadShort();
     94 		$this->yMin = $this->ReadShort();
     95 		$this->xMax = $this->ReadShort();
     96 		$this->yMax = $this->ReadShort();
     97 		$this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint
     98 		$this->indexToLocFormat = $this->ReadShort();
     99 	}
    100 
    101 	function ParseHhea()
    102 	{
    103 		$this->Seek('hhea');
    104 		$this->Skip(4+15*2);
    105 		$this->numberOfHMetrics = $this->ReadUShort();
    106 	}
    107 
    108 	function ParseMaxp()
    109 	{
    110 		$this->Seek('maxp');
    111 		$this->Skip(4);
    112 		$this->numGlyphs = $this->ReadUShort();
    113 	}
    114 
    115 	function ParseHmtx()
    116 	{
    117 		$this->Seek('hmtx');
    118 		$this->glyphs = array();
    119 		for($i=0;$i<$this->numberOfHMetrics;$i++)
    120 		{
    121 			$advanceWidth = $this->ReadUShort();
    122 			$lsb = $this->ReadShort();
    123 			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
    124 		}
    125 		for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++)
    126 		{
    127 			$lsb = $this->ReadShort();
    128 			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
    129 		}
    130 	}
    131 
    132 	function ParseLoca()
    133 	{
    134 		$this->Seek('loca');
    135 		$offsets = array();
    136 		if($this->indexToLocFormat==0)
    137 		{
    138 			// Short format
    139 			for($i=0;$i<=$this->numGlyphs;$i++)
    140 				$offsets[] = 2*$this->ReadUShort();
    141 		}
    142 		else
    143 		{
    144 			// Long format
    145 			for($i=0;$i<=$this->numGlyphs;$i++)
    146 				$offsets[] = $this->ReadULong();
    147 		}
    148 		for($i=0;$i<$this->numGlyphs;$i++)
    149 		{
    150 			$this->glyphs[$i]['offset'] = $offsets[$i];
    151 			$this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i];
    152 		}
    153 	}
    154 
    155 	function ParseGlyf()
    156 	{
    157 		$tableOffset = $this->tables['glyf']['offset'];
    158 		foreach($this->glyphs as &$glyph)
    159 		{
    160 			if($glyph['length']>0)
    161 			{
    162 				fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
    163 				if($this->ReadShort()<0)
    164 				{
    165 					// Composite glyph
    166 					$this->Skip(4*2); // xMin, yMin, xMax, yMax
    167 					$offset = 5*2;
    168 					$a = array();
    169 					do
    170 					{
    171 						$flags = $this->ReadUShort();
    172 						$index = $this->ReadUShort();
    173 						$a[$offset+2] = $index;
    174 						if($flags & 1) // ARG_1_AND_2_ARE_WORDS
    175 							$skip = 2*2;
    176 						else
    177 							$skip = 2;
    178 						if($flags & 8) // WE_HAVE_A_SCALE
    179 							$skip += 2;
    180 						elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE
    181 							$skip += 2*2;
    182 						elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO
    183 							$skip += 4*2;
    184 						$this->Skip($skip);
    185 						$offset += 2*2 + $skip;
    186 					}
    187 					while($flags & 32); // MORE_COMPONENTS
    188 					$glyph['components'] = $a;
    189 				}
    190 			}
    191 		}
    192 	}
    193 
    194 	function ParseCmap()
    195 	{
    196 		$this->Seek('cmap');
    197 		$this->Skip(2); // version
    198 		$numTables = $this->ReadUShort();
    199 		$offset31 = 0;
    200 		for($i=0;$i<$numTables;$i++)
    201 		{
    202 			$platformID = $this->ReadUShort();
    203 			$encodingID = $this->ReadUShort();
    204 			$offset = $this->ReadULong();
    205 			if($platformID==3 && $encodingID==1)
    206 				$offset31 = $offset;
    207 		}
    208 		if($offset31==0)
    209 			$this->Error('No Unicode encoding found');
    210 
    211 		$startCount = array();
    212 		$endCount = array();
    213 		$idDelta = array();
    214 		$idRangeOffset = array();
    215 		$this->chars = array();
    216 		fseek($this->f, $this->tables['cmap']['offset']+$offset31, SEEK_SET);
    217 		$format = $this->ReadUShort();
    218 		if($format!=4)
    219 			$this->Error('Unexpected subtable format: '.$format);
    220 		$this->Skip(2*2); // length, language
    221 		$segCount = $this->ReadUShort()/2;
    222 		$this->Skip(3*2); // searchRange, entrySelector, rangeShift
    223 		for($i=0;$i<$segCount;$i++)
    224 			$endCount[$i] = $this->ReadUShort();
    225 		$this->Skip(2); // reservedPad
    226 		for($i=0;$i<$segCount;$i++)
    227 			$startCount[$i] = $this->ReadUShort();
    228 		for($i=0;$i<$segCount;$i++)
    229 			$idDelta[$i] = $this->ReadShort();
    230 		$offset = ftell($this->f);
    231 		for($i=0;$i<$segCount;$i++)
    232 			$idRangeOffset[$i] = $this->ReadUShort();
    233 
    234 		for($i=0;$i<$segCount;$i++)
    235 		{
    236 			$c1 = $startCount[$i];
    237 			$c2 = $endCount[$i];
    238 			$d = $idDelta[$i];
    239 			$ro = $idRangeOffset[$i];
    240 			if($ro>0)
    241 				fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
    242 			for($c=$c1;$c<=$c2;$c++)
    243 			{
    244 				if($c==0xFFFF)
    245 					break;
    246 				if($ro>0)
    247 				{
    248 					$gid = $this->ReadUShort();
    249 					if($gid>0)
    250 						$gid += $d;
    251 				}
    252 				else
    253 					$gid = $c+$d;
    254 				if($gid>=65536)
    255 					$gid -= 65536;
    256 				if($gid>0)
    257 					$this->chars[$c] = $gid;
    258 			}
    259 		}
    260 	}
    261 
    262 	function ParseName()
    263 	{
    264 		$this->Seek('name');
    265 		$tableOffset = $this->tables['name']['offset'];
    266 		$this->postScriptName = '';
    267 		$this->Skip(2); // format
    268 		$count = $this->ReadUShort();
    269 		$stringOffset = $this->ReadUShort();
    270 		for($i=0;$i<$count;$i++)
    271 		{
    272 			$this->Skip(3*2); // platformID, encodingID, languageID
    273 			$nameID = $this->ReadUShort();
    274 			$length = $this->ReadUShort();
    275 			$offset = $this->ReadUShort();
    276 			if($nameID==6)
    277 			{
    278 				// PostScript name
    279 				fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
    280 				$s = $this->Read($length);
    281 				$s = str_replace(chr(0), '', $s);
    282 				$s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
    283 				$this->postScriptName = $s;
    284 				break;
    285 			}
    286 		}
    287 		if($this->postScriptName=='')
    288 			$this->Error('PostScript name not found');
    289 	}
    290 
    291 	function ParseOS2()
    292 	{
    293 		$this->Seek('OS/2');
    294 		$version = $this->ReadUShort();
    295 		$this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
    296 		$fsType = $this->ReadUShort();
    297 		$this->embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
    298 		$this->Skip(11*2+10+4*4+4);
    299 		$fsSelection = $this->ReadUShort();
    300 		$this->bold = ($fsSelection & 32)!=0;
    301 		$this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
    302 		$this->typoAscender = $this->ReadShort();
    303 		$this->typoDescender = $this->ReadShort();
    304 		if($version>=2)
    305 		{
    306 			$this->Skip(3*2+2*4+2);
    307 			$this->capHeight = $this->ReadShort();
    308 		}
    309 		else
    310 			$this->capHeight = 0;
    311 	}
    312 
    313 	function ParsePost()
    314 	{
    315 		$this->Seek('post');
    316 		$version = $this->ReadULong();
    317 		$this->italicAngle = $this->ReadShort();
    318 		$this->Skip(2); // Skip decimal part
    319 		$this->underlinePosition = $this->ReadShort();
    320 		$this->underlineThickness = $this->ReadShort();
    321 		$this->isFixedPitch = ($this->ReadULong()!=0);
    322 		if($version==0x20000)
    323 		{
    324 			// Extract glyph names
    325 			$this->Skip(4*4); // min/max usage
    326 			$this->Skip(2); // numberOfGlyphs
    327 			$glyphNameIndex = array();
    328 			$names = array();
    329 			$numNames = 0;
    330 			for($i=0;$i<$this->numGlyphs;$i++)
    331 			{
    332 				$index = $this->ReadUShort();
    333 				$glyphNameIndex[] = $index;
    334 				if($index>=258 && $index-257>$numNames)
    335 					$numNames = $index-257;
    336 			}
    337 			for($i=0;$i<$numNames;$i++)
    338 			{
    339 				$len = ord($this->Read(1));
    340 				$names[] = $this->Read($len);
    341 			}
    342 			foreach($glyphNameIndex as $i=>$index)
    343 			{
    344 				if($index>=258)
    345 					$this->glyphs[$i]['name'] = $names[$index-258];
    346 				else
    347 					$this->glyphs[$i]['name'] = $index;
    348 			}
    349 			$this->glyphNames = true;
    350 		}
    351 		else
    352 			$this->glyphNames = false;
    353 	}
    354 
    355 	function Subset($chars)
    356 	{
    357 		$this->subsettedGlyphs = array();
    358 		$this->AddGlyph(0);
    359 		$this->subsettedChars = array();
    360 		foreach($chars as $char)
    361 		{
    362 			if(isset($this->chars[$char]))
    363 			{
    364 				$this->subsettedChars[] = $char;
    365 				$this->AddGlyph($this->chars[$char]);
    366 			}
    367 		}
    368 	}
    369 
    370 	function AddGlyph($id)
    371 	{
    372 		if(!isset($this->glyphs[$id]['ssid']))
    373 		{
    374 			$this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs);
    375 			$this->subsettedGlyphs[] = $id;
    376 			if(isset($this->glyphs[$id]['components']))
    377 			{
    378 				foreach($this->glyphs[$id]['components'] as $cid)
    379 					$this->AddGlyph($cid);
    380 			}
    381 		}
    382 	}
    383 
    384 	function Build()
    385 	{
    386 		$this->BuildCmap();
    387 		$this->BuildHhea();
    388 		$this->BuildHmtx();
    389 		$this->BuildLoca();
    390 		$this->BuildGlyf();
    391 		$this->BuildMaxp();
    392 		$this->BuildPost();
    393 		return $this->BuildFont();
    394 	}
    395 
    396 	function BuildCmap()
    397 	{
    398 		if(!isset($this->subsettedChars))
    399 			return;
    400 
    401 		// Divide charset in contiguous segments
    402 		$chars = $this->subsettedChars;
    403 		sort($chars);
    404 		$segments = array();
    405 		$segment = array($chars[0], $chars[0]);
    406 		for($i=1;$i<count($chars);$i++)
    407 		{
    408 			if($chars[$i]>$segment[1]+1)
    409 			{
    410 				$segments[] = $segment;
    411 				$segment = array($chars[$i], $chars[$i]);
    412 			}
    413 			else
    414 				$segment[1]++;
    415 		}
    416 		$segments[] = $segment;
    417 		$segments[] = array(0xFFFF, 0xFFFF);
    418 		$segCount = count($segments);
    419 
    420 		// Build a Format 4 subtable
    421 		$startCount = array();
    422 		$endCount = array();
    423 		$idDelta = array();
    424 		$idRangeOffset = array();
    425 		$glyphIdArray = '';
    426 		for($i=0;$i<$segCount;$i++)
    427 		{
    428 			list($start, $end) = $segments[$i];
    429 			$startCount[] = $start;
    430 			$endCount[] = $end;
    431 			if($start!=$end)
    432 			{
    433 				// Segment with multiple chars
    434 				$idDelta[] = 0;
    435 				$idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2;
    436 				for($c=$start;$c<=$end;$c++)
    437 				{
    438 					$ssid = $this->glyphs[$this->chars[$c]]['ssid'];
    439 					$glyphIdArray .= pack('n', $ssid);
    440 				}
    441 			}
    442 			else
    443 			{
    444 				// Segment with a single char
    445 				if($start<0xFFFF)
    446 					$ssid = $this->glyphs[$this->chars[$start]]['ssid'];
    447 				else
    448 					$ssid = 0;
    449 				$idDelta[] = $ssid - $start;
    450 				$idRangeOffset[] = 0;
    451 			}
    452 		}
    453 		$entrySelector = 0;
    454 		$n = $segCount;
    455 		while($n!=1)
    456 		{
    457 			$n = $n>>1;
    458 			$entrySelector++;
    459 		}
    460 		$searchRange = (1<<$entrySelector)*2;
    461 		$rangeShift = 2*$segCount - $searchRange;
    462 		$cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift);
    463 		foreach($endCount as $val)
    464 			$cmap .= pack('n', $val);
    465 		$cmap .= pack('n', 0); // reservedPad
    466 		foreach($startCount as $val)
    467 			$cmap .= pack('n', $val);
    468 		foreach($idDelta as $val)
    469 			$cmap .= pack('n', $val);
    470 		foreach($idRangeOffset as $val)
    471 			$cmap .= pack('n', $val);
    472 		$cmap .= $glyphIdArray;
    473 
    474 		$data = pack('nn', 0, 1); // version, numTables
    475 		$data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset
    476 		$data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language
    477 		$data .= $cmap;
    478 		$this->SetTable('cmap', $data);
    479 	}
    480 
    481 	function BuildHhea()
    482 	{
    483 		$this->LoadTable('hhea');
    484 		$numberOfHMetrics = count($this->subsettedGlyphs);
    485 		$data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2);
    486 		$this->SetTable('hhea', $data);
    487 	}
    488 
    489 	function BuildHmtx()
    490 	{
    491 		$data = '';
    492 		foreach($this->subsettedGlyphs as $id)
    493 		{
    494 			$glyph = $this->glyphs[$id];
    495 			$data .= pack('nn', $glyph['w'], $glyph['lsb']);
    496 		}
    497 		$this->SetTable('hmtx', $data);
    498 	}
    499 
    500 	function BuildLoca()
    501 	{
    502 		$data = '';
    503 		$offset = 0;
    504 		foreach($this->subsettedGlyphs as $id)
    505 		{
    506 			if($this->indexToLocFormat==0)
    507 				$data .= pack('n', $offset/2);
    508 			else
    509 				$data .= pack('N', $offset);
    510 			$offset += $this->glyphs[$id]['length'];
    511 		}
    512 		if($this->indexToLocFormat==0)
    513 			$data .= pack('n', $offset/2);
    514 		else
    515 			$data .= pack('N', $offset);
    516 		$this->SetTable('loca', $data);
    517 	}
    518 
    519 	function BuildGlyf()
    520 	{
    521 		$tableOffset = $this->tables['glyf']['offset'];
    522 		$data = '';
    523 		foreach($this->subsettedGlyphs as $id)
    524 		{
    525 			$glyph = $this->glyphs[$id];
    526 			fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
    527 			$glyph_data = $this->Read($glyph['length']);
    528 			if(isset($glyph['components']))
    529 			{
    530 				// Composite glyph
    531 				foreach($glyph['components'] as $offset=>$cid)
    532 				{
    533 					$ssid = $this->glyphs[$cid]['ssid'];
    534 					$glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2);
    535 				}
    536 			}
    537 			$data .= $glyph_data;
    538 		}
    539 		$this->SetTable('glyf', $data);
    540 	}
    541 
    542 	function BuildMaxp()
    543 	{
    544 		$this->LoadTable('maxp');
    545 		$numGlyphs = count($this->subsettedGlyphs);
    546 		$data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2);
    547 		$this->SetTable('maxp', $data);
    548 	}
    549 
    550 	function BuildPost()
    551 	{
    552 		$this->Seek('post');
    553 		if($this->glyphNames)
    554 		{
    555 			// Version 2.0
    556 			$numberOfGlyphs = count($this->subsettedGlyphs);
    557 			$numNames = 0;
    558 			$names = '';
    559 			$data = $this->Read(2*4+2*2+5*4);
    560 			$data .= pack('n', $numberOfGlyphs);
    561 			foreach($this->subsettedGlyphs as $id)
    562 			{
    563 				$name = $this->glyphs[$id]['name'];
    564 				if(is_string($name))
    565 				{
    566 					$data .= pack('n', 258+$numNames);
    567 					$names .= chr(strlen($name)).$name;
    568 					$numNames++;
    569 				}
    570 				else
    571 					$data .= pack('n', $name);
    572 			}
    573 			$data .= $names;
    574 		}
    575 		else
    576 		{
    577 			// Version 3.0
    578 			$this->Skip(4);
    579 			$data = "\x00\x03\x00\x00";
    580 			$data .= $this->Read(4+2*2+5*4);
    581 		}
    582 		$this->SetTable('post', $data);
    583 	}
    584 
    585 	function BuildFont()
    586 	{
    587 		$tags = array();
    588 		foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag)
    589 		{
    590 			if(isset($this->tables[$tag]))
    591 				$tags[] = $tag;
    592 		}
    593 		$numTables = count($tags);
    594 		$offset = 12 + 16*$numTables;
    595 		foreach($tags as $tag)
    596 		{
    597 			if(!isset($this->tables[$tag]['data']))
    598 				$this->LoadTable($tag);
    599 			$this->tables[$tag]['offset'] = $offset;
    600 			$offset += strlen($this->tables[$tag]['data']);
    601 		}
    602 
    603 		// Build offset table
    604 		$entrySelector = 0;
    605 		$n = $numTables;
    606 		while($n!=1)
    607 		{
    608 			$n = $n>>1;
    609 			$entrySelector++;
    610 		}
    611 		$searchRange = 16*(1<<$entrySelector);
    612 		$rangeShift = 16*$numTables - $searchRange;
    613 		$offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift);
    614 		foreach($tags as $tag)
    615 		{
    616 			$table = $this->tables[$tag];
    617 			$offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']);
    618 		}
    619 
    620 		// Compute checkSumAdjustment (0xB1B0AFBA - font checkSum)
    621 		$s = $this->CheckSum($offsetTable);
    622 		foreach($tags as $tag)
    623 			$s .= $this->tables[$tag]['checkSum'];
    624 		$a = unpack('n2', $this->CheckSum($s));
    625 		$high = 0xB1B0 + ($a[1]^0xFFFF);
    626 		$low = 0xAFBA + ($a[2]^0xFFFF) + 1;
    627 		$checkSumAdjustment = pack('nn', $high+($low>>16), $low);
    628 		$this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4);
    629 
    630 		$font = $offsetTable;
    631 		foreach($tags as $tag)
    632 			$font .= $this->tables[$tag]['data'];
    633 
    634 		return $font;
    635 	}
    636 
    637 	function LoadTable($tag)
    638 	{
    639 		$this->Seek($tag);
    640 		$length = $this->tables[$tag]['length'];
    641 		$n = $length % 4;
    642 		if($n>0)
    643 			$length += 4 - $n;
    644 		$this->tables[$tag]['data'] = $this->Read($length);
    645 	}
    646 
    647 	function SetTable($tag, $data)
    648 	{
    649 		$length = strlen($data);
    650 		$n = $length % 4;
    651 		if($n>0)
    652 			$data = str_pad($data, $length+4-$n, "\x00");
    653 		$this->tables[$tag]['data'] = $data;
    654 		$this->tables[$tag]['length'] = $length;
    655 		$this->tables[$tag]['checkSum'] = $this->CheckSum($data);
    656 	}
    657 
    658 	function Seek($tag)
    659 	{
    660 		if(!isset($this->tables[$tag]))
    661 			$this->Error('Table not found: '.$tag);
    662 		fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET);
    663 	}
    664 
    665 	function Skip($n)
    666 	{
    667 		fseek($this->f, $n, SEEK_CUR);
    668 	}
    669 
    670 	function Read($n)
    671 	{
    672 		return $n>0 ? fread($this->f, $n) : '';
    673 	}
    674 
    675 	function ReadUShort()
    676 	{
    677 		$a = unpack('nn', fread($this->f,2));
    678 		return $a['n'];
    679 	}
    680 
    681 	function ReadShort()
    682 	{
    683 		$a = unpack('nn', fread($this->f,2));
    684 		$v = $a['n'];
    685 		if($v>=0x8000)
    686 			$v -= 65536;
    687 		return $v;
    688 	}
    689 
    690 	function ReadULong()
    691 	{
    692 		$a = unpack('NN', fread($this->f,4));
    693 		return $a['N'];
    694 	}
    695 
    696 	function CheckSum($s)
    697 	{
    698 		$n = strlen($s);
    699 		$high = 0;
    700 		$low = 0;
    701 		for($i=0;$i<$n;$i+=4)
    702 		{
    703 			$high += (ord($s[$i])<<8) + ord($s[$i+1]);
    704 			$low += (ord($s[$i+2])<<8) + ord($s[$i+3]);
    705 		}
    706 		return pack('nn', $high+($low>>16), $low);
    707 	}
    708 
    709 	function Error($msg)
    710 	{
    711 		throw new Exception($msg);
    712 	}
    713 }
    714 ?>