| [ Index ] | PHP Cross Reference of Mambo 4.6.5 |
|
| [ Variables ] [ Functions ] [ Classes ] [ Constants ] [ Statistics ] | ||
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Cpdf 4 * 5 * http://www.ros.co.nz/pdf 6 * 7 * A PHP class to provide the basic functionality to create a pdf document without 8 * any requirement for additional modules. 9 * 10 * Note that they companion class CezPdf can be used to extend this class and dramatically 11 * simplify the creation of documents. 12 * 13 * IMPORTANT NOTE 14 * there is no warranty, implied or otherwise with this software. 15 * 16 * LICENCE 17 * This code has been placed in the Public Domain for all to enjoy. 18 * 19 * @author Wayne Munro <pdf@ros.co.nz> 20 * @version 009 21 * @package Cpdf 22 */ 23 class Cpdf { 24 25 /** 26 * the current number of pdf objects in the document 27 */ 28 var $numObj=0; 29 /** 30 * this array contains all of the pdf objects, ready for final assembly 31 */ 32 var $objects = array(); 33 /** 34 * the objectId (number within the objects array) of the document catalog 35 */ 36 var $catalogId; 37 /** 38 * array carrying information about the fonts that the system currently knows about 39 * used to ensure that a font is not loaded twice, among other things 40 */ 41 var $fonts=array(); 42 /** 43 * a record of the current font 44 */ 45 var $currentFont=''; 46 /** 47 * the current base font 48 */ 49 var $currentBaseFont=''; 50 /** 51 * the number of the current font within the font array 52 */ 53 var $currentFontNum=0; 54 /** 55 * 56 */ 57 var $currentNode; 58 /** 59 * object number of the current page 60 */ 61 var $currentPage; 62 /** 63 * object number of the currently active contents block 64 */ 65 var $currentContents; 66 /** 67 * number of fonts within the system 68 */ 69 var $numFonts=0; 70 /** 71 * current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active 72 */ 73 var $currentColour=array('r'=>-1,'g'=>-1,'b'=>-1); 74 /** 75 * current colour for stroke operations (lines etc.) 76 */ 77 var $currentStrokeColour=array('r'=>-1,'g'=>-1,'b'=>-1); 78 /** 79 * current style that lines are drawn in 80 */ 81 var $currentLineStyle=''; 82 /** 83 * an array which is used to save the state of the document, mainly the colours and styles 84 * it is used to temporarily change to another state, the change back to what it was before 85 */ 86 var $stateStack = array(); 87 /** 88 * number of elements within the state stack 89 */ 90 var $nStateStack = 0; 91 /** 92 * number of page objects within the document 93 */ 94 var $numPages=0; 95 /** 96 * object Id storage stack 97 */ 98 var $stack=array(); 99 /** 100 * number of elements within the object Id storage stack 101 */ 102 var $nStack=0; 103 /** 104 * an array which contains information about the objects which are not firmly attached to pages 105 * these have been added with the addObject function 106 */ 107 var $looseObjects=array(); 108 /** 109 * array contains infomation about how the loose objects are to be added to the document 110 */ 111 var $addLooseObjects=array(); 112 /** 113 * the objectId of the information object for the document 114 * this contains authorship, title etc. 115 */ 116 var $infoObject=0; 117 /** 118 * number of images being tracked within the document 119 */ 120 var $numImages=0; 121 /** 122 * an array containing options about the document 123 * it defaults to turning on the compression of the objects 124 */ 125 var $options=array('compression'=>1); 126 /** 127 * the objectId of the first page of the document 128 */ 129 var $firstPageId; 130 /** 131 * used to track the last used value of the inter-word spacing, this is so that it is known 132 * when the spacing is changed. 133 */ 134 var $wordSpaceAdjust=0; 135 /** 136 * the object Id of the procset object 137 */ 138 var $procsetObjectId; 139 /** 140 * store the information about the relationship between font families 141 * this used so that the code knows which font is the bold version of another font, etc. 142 * the value of this array is initialised in the constuctor function. 143 */ 144 var $fontFamilies = array(); 145 /** 146 * track if the current font is bolded or italicised 147 */ 148 var $currentTextState = ''; 149 /** 150 * messages are stored here during processing, these can be selected afterwards to give some useful debug information 151 */ 152 var $messages=''; 153 /** 154 * the ancryption array for the document encryption is stored here 155 */ 156 var $arc4=''; 157 /** 158 * the object Id of the encryption information 159 */ 160 var $arc4_objnum=0; 161 /** 162 * the file identifier, used to uniquely identify a pdf document 163 */ 164 var $fileIdentifier=''; 165 /** 166 * a flag to say if a document is to be encrypted or not 167 */ 168 var $encrypted=0; 169 /** 170 * the ancryption key for the encryption of all the document content (structure is not encrypted) 171 */ 172 var $encryptionKey=''; 173 /** 174 * array which forms a stack to keep track of nested callback functions 175 */ 176 var $callback = array(); 177 /** 178 * the number of callback functions in the callback array 179 */ 180 var $nCallback = 0; 181 /** 182 * store label->id pairs for named destinations, these will be used to replace internal links 183 * done this way so that destinations can be defined after the location that links to them 184 */ 185 var $destinations = array(); 186 /** 187 * store the stack for the transaction commands, each item in here is a record of the values of all the 188 * variables within the class, so that the user can rollback at will (from each 'start' command) 189 * note that this includes the objects array, so these can be large. 190 */ 191 var $checkpoint = ''; 192 /** 193 * class constructor 194 * this will start a new document 195 * @var array array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero. 196 */ 197 function Cpdf ($pageSize=array(0,0,612,792)){ 198 $this->newDocument($pageSize); 199 200 // also initialize the font families that are known about already 201 $this->setFontFamily('init'); 202 // $this->fileIdentifier = md5('xxxxxxxx'.time()); 203 204 } 205 206 /** 207 * Document object methods (internal use only) 208 * 209 * There is about one object method for each type of object in the pdf document 210 * Each function has the same call list ($id,$action,$options). 211 * $id = the object ID of the object, or what it is to be if it is being created 212 * $action = a string specifying the action to be performed, though ALL must support: 213 * 'new' - create the object with the id $id 214 * 'out' - produce the output for the pdf object 215 * $options = optional, a string or array containing the various parameters for the object 216 * 217 * These, in conjunction with the output function are the ONLY way for output to be produced 218 * within the pdf 'file'. 219 */ 220 221 /** 222 *destination object, used to specify the location for the user to jump to, presently on opening 223 */ 224 function o_destination($id,$action,$options=''){ 225 if ($action!='new'){ 226 $o =& $this->objects[$id]; 227 } 228 switch($action){ 229 case 'new': 230 $this->objects[$id]=array('t'=>'destination','info'=>array()); 231 $tmp = ''; 232 switch ($options['type']){ 233 case 'XYZ': 234 case 'FitR': 235 $tmp = ' '.$options['p3'].$tmp; 236 case 'FitH': 237 case 'FitV': 238 case 'FitBH': 239 case 'FitBV': 240 $tmp = ' '.$options['p1'].' '.$options['p2'].$tmp; 241 case 'Fit': 242 case 'FitB': 243 $tmp = $options['type'].$tmp; 244 $this->objects[$id]['info']['string']=$tmp; 245 $this->objects[$id]['info']['page']=$options['page']; 246 } 247 break; 248 case 'out': 249 $tmp = $o['info']; 250 $res="\n".$id." 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj\n"; 251 return $res; 252 break; 253 } 254 } 255 256 /** 257 * set the viewer preferences 258 */ 259 function o_viewerPreferences($id,$action,$options=''){ 260 if ($action!='new'){ 261 $o =& $this->objects[$id]; 262 } 263 switch ($action){ 264 case 'new': 265 $this->objects[$id]=array('t'=>'viewerPreferences','info'=>array()); 266 break; 267 case 'add': 268 foreach($options as $k=>$v){ 269 switch ($k){ 270 case 'HideToolbar': 271 case 'HideMenubar': 272 case 'HideWindowUI': 273 case 'FitWindow': 274 case 'CenterWindow': 275 case 'NonFullScreenPageMode': 276 case 'Direction': 277 $o['info'][$k]=$v; 278 break; 279 } 280 } 281 break; 282 case 'out': 283 284 $res="\n".$id." 0 obj\n".'<< '; 285 foreach($o['info'] as $k=>$v){ 286 $res.="\n/".$k.' '.$v; 287 } 288 $res.="\n>>\n"; 289 return $res; 290 break; 291 } 292 } 293 294 /** 295 * define the document catalog, the overall controller for the document 296 */ 297 function o_catalog($id,$action,$options=''){ 298 if ($action!='new'){ 299 $o =& $this->objects[$id]; 300 } 301 switch ($action){ 302 case 'new': 303 $this->objects[$id]=array('t'=>'catalog','info'=>array()); 304 $this->catalogId=$id; 305 break; 306 case 'outlines': 307 case 'pages': 308 case 'openHere': 309 $o['info'][$action]=$options; 310 break; 311 case 'viewerPreferences': 312 if (!isset($o['info']['viewerPreferences'])){ 313 $this->numObj++; 314 $this->o_viewerPreferences($this->numObj,'new'); 315 $o['info']['viewerPreferences']=$this->numObj; 316 } 317 $vp = $o['info']['viewerPreferences']; 318 $this->o_viewerPreferences($vp,'add',$options); 319 break; 320 case 'out': 321 $res="\n".$id." 0 obj\n".'<< /Type /Catalog'; 322 foreach($o['info'] as $k=>$v){ 323 switch($k){ 324 case 'outlines': 325 $res.="\n".'/Outlines '.$v.' 0 R'; 326 break; 327 case 'pages': 328 $res.="\n".'/Pages '.$v.' 0 R'; 329 break; 330 case 'viewerPreferences': 331 $res.="\n".'/ViewerPreferences '.$o['info']['viewerPreferences'].' 0 R'; 332 break; 333 case 'openHere': 334 $res.="\n".'/OpenAction '.$o['info']['openHere'].' 0 R'; 335 break; 336 } 337 } 338 $res.=" >>\nendobj"; 339 return $res; 340 break; 341 } 342 } 343 344 /** 345 * object which is a parent to the pages in the document 346 */ 347 function o_pages($id,$action,$options=''){ 348 if ($action!='new'){ 349 $o =& $this->objects[$id]; 350 } 351 switch ($action){ 352 case 'new': 353 $this->objects[$id]=array('t'=>'pages','info'=>array()); 354 $this->o_catalog($this->catalogId,'pages',$id); 355 break; 356 case 'page': 357 if (!is_array($options)){ 358 // then it will just be the id of the new page 359 $o['info']['pages'][]=$options; 360 } else { 361 // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative 362 // and pos is either 'before' or 'after', saying where this page will fit. 363 if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])){ 364 $i = array_search($options['rid'],$o['info']['pages']); 365 if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i]==$options['rid']){ 366 // then there is a match 367 // make a space 368 switch ($options['pos']){ 369 case 'before': 370 $k = $i; 371 break; 372 case 'after': 373 $k=$i+1; 374 break; 375 default: 376 $k=-1; 377 break; 378 } 379 if ($k>=0){ 380 for ($j=count($o['info']['pages'])-1;$j>=$k;$j--){ 381 $o['info']['pages'][$j+1]=$o['info']['pages'][$j]; 382 } 383 $o['info']['pages'][$k]=$options['id']; 384 } 385 } 386 } 387 } 388 break; 389 case 'procset': 390 $o['info']['procset']=$options; 391 break; 392 case 'mediaBox': 393 $o['info']['mediaBox']=$options; // which should be an array of 4 numbers 394 break; 395 case 'font': 396 $o['info']['fonts'][]=array('objNum'=>$options['objNum'],'fontNum'=>$options['fontNum']); 397 break; 398 case 'xObject': 399 $o['info']['xObjects'][]=array('objNum'=>$options['objNum'],'label'=>$options['label']); 400 break; 401 case 'out': 402 if (count($o['info']['pages'])){ 403 $res="\n".$id." 0 obj\n<< /Type /Pages\n/Kids ["; 404 foreach($o['info']['pages'] as $k=>$v){ 405 $res.=$v." 0 R\n"; 406 } 407 $res.="]\n/Count ".count($this->objects[$id]['info']['pages']); 408 if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || isset($o['info']['procset'])){ 409 $res.="\n/Resources <<"; 410 if (isset($o['info']['procset'])){ 411 $res.="\n/ProcSet ".$o['info']['procset']." 0 R"; 412 } 413 if (isset($o['info']['fonts']) && count($o['info']['fonts'])){ 414 $res.="\n/Font << "; 415 foreach($o['info']['fonts'] as $finfo){ 416 $res.="\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R"; 417 } 418 $res.=" >>"; 419 } 420 if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])){ 421 $res.="\n/XObject << "; 422 foreach($o['info']['xObjects'] as $finfo){ 423 $res.="\n/".$finfo['label']." ".$finfo['objNum']." 0 R"; 424 } 425 $res.=" >>"; 426 } 427 $res.="\n>>"; 428 if (isset($o['info']['mediaBox'])){ 429 $tmp=$o['info']['mediaBox']; 430 $res.="\n/MediaBox [".sprintf('%.3f',$tmp[0]).' '.sprintf('%.3f',$tmp[1]).' '.sprintf('%.3f',$tmp[2]).' '.sprintf('%.3f',$tmp[3]).']'; 431 } 432 } 433 $res.="\n >>\nendobj"; 434 } else { 435 $res="\n".$id." 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj"; 436 } 437 return $res; 438 break; 439 } 440 } 441 442 /** 443 * define the outlines in the doc, empty for now 444 */ 445 function o_outlines($id,$action,$options=''){ 446 if ($action!='new'){ 447 $o =& $this->objects[$id]; 448 } 449 switch ($action){ 450 case 'new': 451 $this->objects[$id]=array('t'=>'outlines','info'=>array('outlines'=>array())); 452 $this->o_catalog($this->catalogId,'outlines',$id); 453 break; 454 case 'outline': 455 $o['info']['outlines'][]=$options; 456 break; 457 case 'out': 458 if (count($o['info']['outlines'])){ 459 $res="\n".$id." 0 obj\n<< /Type /Outlines /Kids ["; 460 foreach($o['info']['outlines'] as $k=>$v){ 461 $res.=$v." 0 R "; 462 } 463 $res.="] /Count ".count($o['info']['outlines'])." >>\nendobj"; 464 } else { 465 $res="\n".$id." 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj"; 466 } 467 return $res; 468 break; 469 } 470 } 471 472 /** 473 * an object to hold the font description 474 */ 475 function o_font($id,$action,$options=''){ 476 if ($action!='new'){ 477 $o =& $this->objects[$id]; 478 } 479 switch ($action){ 480 case 'new': 481 $this->objects[$id]=array('t'=>'font','info'=>array('name'=>$options['name'],'SubType'=>'Type1')); 482 $fontNum=$this->numFonts; 483 $this->objects[$id]['info']['fontNum']=$fontNum; 484 // deal with the encoding and the differences 485 if (isset($options['differences'])){ 486 // then we'll need an encoding dictionary 487 $this->numObj++; 488 $this->o_fontEncoding($this->numObj,'new',$options); 489 $this->objects[$id]['info']['encodingDictionary']=$this->numObj; 490 } else if (isset($options['encoding'])){ 491 // we can specify encoding here 492 switch($options['encoding']){ 493 case 'WinAnsiEncoding': 494 case 'MacRomanEncoding': 495 case 'MacExpertEncoding': 496 $this->objects[$id]['info']['encoding']=$options['encoding']; 497 break; 498 case 'none': 499 break; 500 default: 501 $this->objects[$id]['info']['encoding']='WinAnsiEncoding'; 502 break; 503 } 504 } else { 505 $this->objects[$id]['info']['encoding']='WinAnsiEncoding'; 506 } 507 // also tell the pages node about the new font 508 $this->o_pages($this->currentNode,'font',array('fontNum'=>$fontNum,'objNum'=>$id)); 509 break; 510 case 'add': 511 foreach ($options as $k=>$v){ 512 switch ($k){ 513 case 'BaseFont': 514 $o['info']['name'] = $v; 515 break; 516 case 'FirstChar': 517 case 'LastChar': 518 case 'Widths': 519 case 'FontDescriptor': 520 case 'SubType': 521 $this->addMessage('o_font '.$k." : ".$v); 522 $o['info'][$k] = $v; 523 break; 524 } 525 } 526 break; 527 case 'out': 528 $res="\n".$id." 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n"; 529 $res.="/Name /F".$o['info']['fontNum']."\n"; 530 $res.="/BaseFont /".$o['info']['name']."\n"; 531 if (isset($o['info']['encodingDictionary'])){ 532 // then place a reference to the dictionary 533 $res.="/Encoding ".$o['info']['encodingDictionary']." 0 R\n"; 534 } else if (isset($o['info']['encoding'])){ 535 // use the specified encoding 536 $res.="/Encoding /".$o['info']['encoding']."\n"; 537 } 538 if (isset($o['info']['FirstChar'])){ 539 $res.="/FirstChar ".$o['info']['FirstChar']."\n"; 540 } 541 if (isset($o['info']['LastChar'])){ 542 $res.="/LastChar ".$o['info']['LastChar']."\n"; 543 } 544 if (isset($o['info']['Widths'])){ 545 $res.="/Widths ".$o['info']['Widths']." 0 R\n"; 546 } 547 if (isset($o['info']['FontDescriptor'])){ 548 $res.="/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n"; 549 } 550 $res.=">>\nendobj"; 551 return $res; 552 break; 553 } 554 } 555 556 /** 557 * a font descriptor, needed for including additional fonts 558 */ 559 function o_fontDescriptor($id,$action,$options=''){ 560 if ($action!='new'){ 561 $o =& $this->objects[$id]; 562 } 563 switch ($action){ 564 case 'new': 565 $this->objects[$id]=array('t'=>'fontDescriptor','info'=>$options); 566 break; 567 case 'out': 568 $res="\n".$id." 0 obj\n<< /Type /FontDescriptor\n"; 569 foreach ($o['info'] as $label => $value){ 570 switch ($label){ 571 case 'Ascent': 572 case 'CapHeight': 573 case 'Descent': 574 case 'Flags': 575 case 'ItalicAngle': 576 case 'StemV': 577 case 'AvgWidth': 578 case 'Leading': 579 case 'MaxWidth': 580 case 'MissingWidth': 581 case 'StemH': 582 case 'XHeight': 583 case 'CharSet': 584 if (strlen($value)){ 585 $res.='/'.$label.' '.$value."\n"; 586 } 587 break; 588 case 'FontFile': 589 case 'FontFile2': 590 case 'FontFile3': 591 $res.='/'.$label.' '.$value." 0 R\n"; 592 break; 593 case 'FontBBox': 594 $res.='/'.$label.' ['.$value[0].' '.$value[1].' '.$value[2].' '.$value[3]."]\n"; 595 break; 596 case 'FontName': 597 $res.='/'.$label.' /'.$value."\n"; 598 break; 599 } 600 } 601 $res.=">>\nendobj"; 602 return $res; 603 break; 604 } 605 } 606 607 /** 608 * the font encoding 609 */ 610 function o_fontEncoding($id,$action,$options=''){ 611 if ($action!='new'){ 612 $o =& $this->objects[$id]; 613 } 614 switch ($action){ 615 case 'new': 616 // the options array should contain 'differences' and maybe 'encoding' 617 $this->objects[$id]=array('t'=>'fontEncoding','info'=>$options); 618 break; 619 case 'out': 620 $res="\n".$id." 0 obj\n<< /Type /Encoding\n"; 621 if (!isset($o['info']['encoding'])){ 622 $o['info']['encoding']='WinAnsiEncoding'; 623 } 624 if ($o['info']['encoding']!='none'){ 625 $res.="/BaseEncoding /".$o['info']['encoding']."\n"; 626 } 627 $res.="/Differences \n["; 628 $onum=-100; 629 foreach($o['info']['differences'] as $num=>$label){ 630 if ($num!=$onum+1){ 631 // we cannot make use of consecutive numbering 632 $res.= "\n".$num." /".$label; 633 } else { 634 $res.= " /".$label; 635 } 636 $onum=$num; 637 } 638 $res.="\n]\n>>\nendobj"; 639 return $res; 640 break; 641 } 642 } 643 644 /** 645 * the document procset, solves some problems with printing to old PS printers 646 */ 647 function o_procset($id,$action,$options=''){ 648 if ($action!='new'){ 649 $o =& $this->objects[$id]; 650 } 651 switch ($action){ 652 case 'new': 653 $this->objects[$id]=array('t'=>'procset','info'=>array('PDF'=>1,'Text'=>1)); 654 $this->o_pages($this->currentNode,'procset',$id); 655 $this->procsetObjectId=$id; 656 break; 657 case 'add': 658 // this is to add new items to the procset list, despite the fact that this is considered 659 // obselete, the items are required for printing to some postscript printers 660 switch ($options) { 661 case 'ImageB': 662 case 'ImageC': 663 case 'ImageI': 664 $o['info'][$options]=1; 665 break; 666 } 667 break; 668 case 'out': 669 $res="\n".$id." 0 obj\n["; 670 foreach ($o['info'] as $label=>$val){ 671 $res.='/'.$label.' '; 672 } 673 $res.="]\nendobj"; 674 return $res; 675 break; 676 } 677 } 678 679 /** 680 * define the document information 681 */ 682 function o_info($id,$action,$options=''){ 683 if ($action!='new'){ 684 $o =& $this->objects[$id]; 685 } 686 switch ($action){ 687 case 'new': 688 $this->infoObject=$id; 689 $date='D:'.date('Ymd'); 690 $this->objects[$id]=array('t'=>'info','info'=>array('Creator'=>'R and OS php pdf writer, http://www.ros.co.nz','CreationDate'=>$date)); 691 break; 692 case 'Title': 693 case 'Author': 694 case 'Subject': 695 case 'Keywords': 696 case 'Creator': 697 case 'Producer': 698 case 'CreationDate': 699 case 'ModDate': 700 case 'Trapped': 701 $o['info'][$action]=$options; 702 break; 703 case 'out': 704 if ($this->encrypted){ 705 $this->encryptInit($id); 706 } 707 $res="\n".$id." 0 obj\n<<\n"; 708 foreach ($o['info'] as $k=>$v){ 709 $res.='/'.$k.' ('; 710 if ($this->encrypted){ 711 $res.=$this->filterText($this->ARC4($v)); 712 } else { 713 $res.=$this->filterText($v); 714 } 715 $res.=")\n"; 716 } 717 $res.=">>\nendobj"; 718 return $res; 719 break; 720 } 721 } 722 723 /** 724 * an action object, used to link to URLS initially 725 */ 726 function o_action($id,$action,$options=''){ 727 if ($action!='new'){ 728 $o =& $this->objects[$id]; 729 } 730 switch ($action){ 731 case 'new': 732 if (is_array($options)){ 733 $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>$options['type']); 734 } else { 735 // then assume a URI action 736 $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>'URI'); 737 } 738 break; 739 case 'out': 740 if ($this->encrypted){ 741 $this->encryptInit($id); 742 } 743 $res="\n".$id." 0 obj\n<< /Type /Action"; 744 switch($o['type']){ 745 case 'ilink': 746 // there will be an 'label' setting, this is the name of the destination 747 $res.="\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R"; 748 break; 749 case 'URI': 750 $res.="\n/S /URI\n/URI ("; 751 if ($this->encrypted){ 752 $res.=$this->filterText($this->ARC4($o['info'])); 753 } else { 754 $res.=$this->filterText($o['info']); 755 } 756 $res.=")"; 757 break; 758 } 759 $res.="\n>>\nendobj"; 760 return $res; 761 break; 762 } 763 } 764 765 /** 766 * an annotation object, this will add an annotation to the current page. 767 * initially will support just link annotations 768 */ 769 function o_annotation($id,$action,$options=''){ 770 if ($action!='new'){ 771 $o =& $this->objects[$id]; 772 } 773 switch ($action){ 774 case 'new': 775 // add the annotation to the current page 776 $pageId = $this->currentPage; 777 $this->o_page($pageId,'annot',$id); 778 // and add the action object which is going to be required 779 switch($options['type']){ 780 case 'link': 781 $this->objects[$id]=array('t'=>'annotation','info'=>$options); 782 $this->numObj++; 783 $this->o_action($this->numObj,'new',$options['url']); 784 $this->objects[$id]['info']['actionId']=$this->numObj; 785 break; 786 case 'ilink': 787 // this is to a named internal link 788 $label = $options['label']; 789 $this->objects[$id]=array('t'=>'annotation','info'=>$options); 790 $this->numObj++; 791 $this->o_action($this->numObj,'new',array('type'=>'ilink','label'=>$label)); 792 $this->objects[$id]['info']['actionId']=$this->numObj; 793 break; 794 } 795 break; 796 case 'out': 797 $res="\n".$id." 0 obj\n<< /Type /Annot"; 798 switch($o['info']['type']){ 799 case 'link': 800 case 'ilink': 801 $res.= "\n/Subtype /Link"; 802 break; 803 } 804 $res.="\n/A ".$o['info']['actionId']." 0 R"; 805 $res.="\n/Border [0 0 0]"; 806 $res.="\n/H /I"; 807 $res.="\n/Rect [ "; 808 foreach($o['info']['rect'] as $v){ 809 $res.= sprintf("%.4f ",$v); 810 } 811 $res.="]"; 812 $res.="\n>>\nendobj"; 813 return $res; 814 break; 815 } 816 } 817 818 /** 819 * a page object, it also creates a contents object to hold its contents 820 */ 821 function o_page($id,$action,$options=''){ 822 if ($action!='new'){ 823 $o =& $this->objects[$id]; 824 } 825 switch ($action){ 826 case 'new': 827 $this->numPages++; 828 $this->objects[$id]=array('t'=>'page','info'=>array('parent'=>$this->currentNode,'pageNum'=>$this->numPages)); 829 if (is_array($options)){ 830 // then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after] 831 $options['id']=$id; 832 $this->o_pages($this->currentNode,'page',$options); 833 } else { 834 $this->o_pages($this->currentNode,'page',$id); 835 } 836 $this->currentPage=$id; 837 //make a contents object to go with this page 838 $this->numObj++; 839 $this->o_contents($this->numObj,'new',$id); 840 $this->currentContents=$this->numObj; 841 $this->objects[$id]['info']['contents']=array(); 842 $this->objects[$id]['info']['contents'][]=$this->numObj; 843 $match = ($this->numPages%2 ? 'odd' : 'even'); 844 foreach($this->addLooseObjects as $oId=>$target){ 845 if ($target=='all' || $match==$target){ 846 $this->objects[$id]['info']['contents'][]=$oId; 847 } 848 } 849 break; 850 case 'content': 851 $o['info']['contents'][]=$options; 852 break; 853 case 'annot': 854 // add an annotation to this page 855 if (!isset($o['info']['annot'])){ 856 $o['info']['annot']=array(); 857 } 858 // $options should contain the id of the annotation dictionary 859 $o['info']['annot'][]=$options; 860 break; 861 case 'out': 862 $res="\n".$id." 0 obj\n<< /Type /Page"; 863 $res.="\n/Parent ".$o['info']['parent']." 0 R"; 864 if (isset($o['info']['annot'])){ 865 $res.="\n/Annots ["; 866 foreach($o['info']['annot'] as $aId){ 867 $res.=" ".$aId." 0 R"; 868 } 869 $res.=" ]"; 870 } 871 $count = count($o['info']['contents']); 872 if ($count==1){ 873 $res.="\n/Contents ".$o['info']['contents'][0]." 0 R"; 874 } else if ($count>1){ 875 $res.="\n/Contents [\n"; 876 foreach ($o['info']['contents'] as $cId){ 877 $res.=$cId." 0 R\n"; 878 } 879 $res.="]"; 880 } 881 $res.="\n>>\nendobj"; 882 return $res; 883 break; 884 } 885 } 886 887 /** 888 * the contents objects hold all of the content which appears on pages 889 */ 890 function o_contents($id,$action,$options=''){ 891 if ($action!='new'){ 892 $o =& $this->objects[$id]; 893 } 894 switch ($action){ 895 case 'new': 896 $this->objects[$id]=array('t'=>'contents','c'=>'','info'=>array()); 897 if (strlen($options) && intval($options)){ 898 // then this contents is the primary for a page 899 $this->objects[$id]['onPage']=$options; 900 } else if ($options=='raw'){ 901 // then this page contains some other type of system object 902 $this->objects[$id]['raw']=1; 903 } 904 break; 905 case 'add': 906 // add more options to the decleration 907 foreach ($options as $k=>$v){ 908 $o['info'][$k]=$v; 909 } 910 case 'out': 911 $tmp=$o['c']; 912 $res= "\n".$id." 0 obj\n"; 913 if (isset($this->objects[$id]['raw'])){ 914 $res.=$tmp; 915 } else { 916 $res.= "<<"; 917 if (function_exists('gzcompress') && $this->options['compression']){ 918 // then implement ZLIB based compression on this content stream 919 $res.=" /Filter /FlateDecode"; 920 $tmp = gzcompress($tmp); 921 } 922 if ($this->encrypted){ 923 $this->encryptInit($id); 924 $tmp = $this->ARC4($tmp); 925 } 926 foreach($o['info'] as $k=>$v){ 927 $res .= "\n/".$k.' '.$v; 928 } 929 $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream"; 930 } 931 $res.="\nendobj\n"; 932 return $res; 933 break; 934 } 935 } 936 937 /** 938 * an image object, will be an XObject in the document, includes description and data 939 */ 940 function o_image($id,$action,$options=''){ 941 if ($action!='new'){ 942 $o =& $this->objects[$id]; 943 } 944 switch($action){ 945 case 'new': 946 // make the new object 947 $this->objects[$id]=array('t'=>'image','data'=>$options['data'],'info'=>array()); 948 $this->objects[$id]['info']['Type']='/XObject'; 949 $this->objects[$id]['info']['Subtype']='/Image'; 950 $this->objects[$id]['info']['Width']=$options['iw']; 951 $this->objects[$id]['info']['Height']=$options['ih']; 952 if (!isset($options['type']) || $options['type']=='jpg'){ 953 if (!isset($options['channels'])){ 954 $options['channels']=3; 955 } 956 switch($options['channels']){ 957 case 1: 958 $this->objects[$id]['info']['ColorSpace']='/DeviceGray'; 959 break; 960 default: 961 $this->objects[$id]['info']['ColorSpace']='/DeviceRGB'; 962 break; 963 } 964 $this->objects[$id]['info']['Filter']='/DCTDecode'; 965 $this->objects[$id]['info']['BitsPerComponent']=8; 966 } else if ($options['type']=='png'){ 967 $this->objects[$id]['info']['Filter']='/FlateDecode'; 968 $this->objects[$id]['info']['DecodeParms']='<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>'; 969 if (strlen($options['pdata'])){ 970 $tmp = ' [ /Indexed /DeviceRGB '.(strlen($options['pdata'])/3-1).' '; 971 $this->numObj++; 972 $this->o_contents($this->numObj,'new'); 973 $this->objects[$this->numObj]['c']=$options['pdata']; 974 $tmp.=$this->numObj.' 0 R'; 975 $tmp .=' ]'; 976 $this->objects[$id]['info']['ColorSpace'] = $tmp; 977 if (isset($options['transparency'])){ 978 switch($options['transparency']['type']){ 979 case 'indexed': 980 $tmp=' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] '; 981 $this->objects[$id]['info']['Mask'] = $tmp; 982 break; 983 } 984 } 985 } else { 986 $this->objects[$id]['info']['ColorSpace']='/'.$options['color']; 987 } 988 $this->objects[$id]['info']['BitsPerComponent']=$options['bitsPerComponent']; 989 } 990 // assign it a place in the named resource dictionary as an external object, according to 991 // the label passed in with it. 992 $this->o_pages($this->currentNode,'xObject',array('label'=>$options['label'],'objNum'=>$id)); 993 // also make sure that we have the right procset object for it. 994 $this->o_procset($this->procsetObjectId,'add','ImageC'); 995 break; 996 case 'out': 997 $tmp=$o['data']; 998 $res= "\n".$id." 0 obj\n<<"; 999 foreach($o['info'] as $k=>$v){ 1000 $res.="\n/".$k.' '.$v; 1001 } 1002 if ($this->encrypted){ 1003 $this->encryptInit($id); 1004 $tmp = $this->ARC4($tmp); 1005 } 1006 $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream\nendobj\n"; 1007 return $res; 1008 break; 1009 } 1010 } 1011 1012 /** 1013 * encryption object. 1014 */ 1015 function o_encryption($id,$action,$options=''){ 1016 if ($action!='new'){ 1017 $o =& $this->objects[$id]; 1018 } 1019 switch($action){ 1020 case 'new': 1021 // make the new object 1022 $this->objects[$id]=array('t'=>'encryption','info'=>$options); 1023 $this->arc4_objnum=$id; 1024 // figure out the additional paramaters required 1025 $pad = chr(0x28).chr(0xBF).chr(0x4E).chr(0x5E).chr(0x4E).chr(0x75).chr(0x8A).chr(0x41).chr(0x64).chr(0x00).chr(0x4E).chr(0x56).chr(0xFF).chr(0xFA).chr(0x01).chr(0x08).chr(0x2E).chr(0x2E).chr(0x00).chr(0xB6).chr(0xD0).chr(0x68).chr(0x3E).chr(0x80).chr(0x2F).chr(0x0C).chr(0xA9).chr(0xFE).chr(0x64).chr(0x53).chr(0x69).chr(0x7A); 1026 $len = strlen($options['owner']); 1027 if ($len>32){ 1028 $owner = substr($options['owner'],0,32); 1029 } else if ($len<32){ 1030 $owner = $options['owner'].substr($pad,0,32-$len); 1031 } else { 1032 $owner = $options['owner']; 1033 } 1034 $len = strlen($options['user']); 1035 if ($len>32){ 1036 $user = substr($options['user'],0,32); 1037 } else if ($len<32){ 1038 $user = $options['user'].substr($pad,0,32-$len); 1039 } else { 1040 $user = $options['user']; 1041 } 1042 $tmp = $this->md5_16($owner); 1043 $okey = substr($tmp,0,5); 1044 $this->ARC4_init($okey); 1045 $ovalue=$this->ARC4($user); 1046 $this->objects[$id]['info']['O']=$ovalue; 1047 // now make the u value, phew. 1048 $tmp = $this->md5_16($user.$ovalue.chr($options['p']).chr(255).chr(255).chr(255).$this->fileIdentifier); 1049 $ukey = substr($tmp,0,5); 1050 1051 $this->ARC4_init($ukey); 1052 $this->encryptionKey = $ukey; 1053 $this->encrypted=1; 1054 $uvalue=$this->ARC4($pad); 1055 1056 $this->objects[$id]['info']['U']=$uvalue; 1057 $this->encryptionKey=$ukey; 1058 1059 // initialize the arc4 array 1060 break; 1061 case 'out': 1062 $res= "\n".$id." 0 obj\n<<"; 1063 $res.="\n/Filter /Standard"; 1064 $res.="\n/V 1"; 1065 $res.="\n/R 2"; 1066 $res.="\n/O (".$this->filterText($o['info']['O']).')'; 1067 $res.="\n/U (".$this->filterText($o['info']['U']).')'; 1068 // and the p-value needs to be converted to account for the twos-complement approach 1069 $o['info']['p'] = (($o['info']['p']^255)+1)*-1; 1070 $res.="\n/P ".($o['info']['p']); 1071 $res.="\n>>\nendobj\n"; 1072 1073 return $res; 1074 break; 1075 } 1076 } 1077 1078 /** 1079 * ARC4 functions 1080 * A series of function to implement ARC4 encoding in PHP 1081 */ 1082 1083 /** 1084 * calculate the 16 byte version of the 128 bit md5 digest of the string 1085 */ 1086 function md5_16($string){ 1087 $tmp = md5($string); 1088 $out=''; 1089 for ($i=0;$i<=30;$i=$i+2){ 1090 $out.=chr(hexdec(substr($tmp,$i,2))); 1091 } 1092 return $out; 1093 } 1094 1095 /** 1096 * initialize the encryption for processing a particular object 1097 */ 1098 function encryptInit($id){ 1099 $tmp = $this->encryptionKey; 1100 $hex = dechex($id); 1101 if (strlen($hex)<6){ 1102 $hex = substr('000000',0,6-strlen($hex)).$hex; 1103 } 1104 $tmp.= chr(hexdec(substr($hex,4,2))).chr(hexdec(substr($hex,2,2))).chr(hexdec(substr($hex,0,2))).chr(0).chr(0); 1105 $key = $this->md5_16($tmp); 1106 $this->ARC4_init(substr($key,0,10)); 1107 } 1108 1109 /** 1110 * initialize the ARC4 encryption 1111 */ 1112 function ARC4_init($key=''){ 1113 $this->arc4 = ''; 1114 // setup the control array 1115 if (strlen($key)==0){ 1116 return; 1117 } 1118 $k = ''; 1119 while(strlen($k)<256){ 1120 $k.=$key; 1121 } 1122 $k=substr($k,0,256); 1123 for ($i=0;$i<256;$i++){ 1124 $this->arc4 .= chr($i); 1125 } 1126 $j=0; 1127 for ($i=0;$i<256;$i++){ 1128 $t = $this->arc4[$i]; 1129 $j = ($j + ord($t) + ord($k[$i]))%256; 1130 $this->arc4[$i]=$this->arc4[$j]; 1131 $this->arc4[$j]=$t; 1132 } 1133 } 1134 1135 /** 1136 * ARC4 encrypt a text string 1137 */ 1138 function ARC4($text){ 1139 $len=strlen($text); 1140 $a=0; 1141 $b=0; 1142 $c = $this->arc4; 1143 $out=''; 1144 for ($i=0;$i<$len;$i++){ 1145 $a = ($a+1)%256; 1146 $t= $c[$a]; 1147 $b = ($b+ord($t))%256; 1148 $c[$a]=$c[$b]; 1149 $c[$b]=$t; 1150 $k = ord($c[(ord($c[$a])+ord($c[$b]))%256]); 1151 $out.=chr(ord($text[$i]) ^ $k); 1152 } 1153 1154 return $out; 1155 } 1156 1157 /** 1158 * functions which can be called to adjust or add to the document 1159 */ 1160 1161 /** 1162 * add a link in the document to an external URL 1163 */ 1164 function addLink($url,$x0,$y0,$x1,$y1){ 1165 $this->numObj++; 1166 $info = array('type'=>'link','url'=>$url,'rect'=>array($x0,$y0,$x1,$y1)); 1167 $this->o_annotation($this->numObj,'new',$info); 1168 } 1169 1170 /** 1171 * add a link in the document to an internal destination (ie. within the document) 1172 */ 1173 function addInternalLink($label,$x0,$y0,$x1,$y1){ 1174 $this->numObj++; 1175 $info = array('type'=>'ilink','label'=>$label,'rect'=>array($x0,$y0,$x1,$y1)); 1176 $this->o_annotation($this->numObj,'new',$info); 1177 } 1178 1179 /** 1180 * set the encryption of the document 1181 * can be used to turn it on and/or set the passwords which it will have. 1182 * also the functions that the user will have are set here, such as print, modify, add 1183 */ 1184 function setEncryption($userPass='',$ownerPass='',$pc=array()){ 1185 $p=bindec(11000000); 1186 1187 $options = array( 1188 'print'=>4 1189 ,'modify'=>8 1190 ,'copy'=>16 1191 ,'add'=>32 1192 ); 1193 foreach($pc as $k=>$v){ 1194 if ($v && isset($options[$k])){ 1195 $p+=$options[$k]; 1196 } else if (isset($options[$v])){ 1197 $p+=$options[$v]; 1198 } 1199 } 1200 // implement encryption on the document 1201 if ($this->arc4_objnum == 0){ 1202 // then the block does not exist already, add it. 1203 $this->numObj++; 1204 if (strlen($ownerPass)==0){ 1205 $ownerPass=$userPass; 1206 } 1207 $this->o_encryption($this->numObj,'new',array('user'=>$userPass,'owner'=>$ownerPass,'p'=>$p)); 1208 } 1209 } 1210 1211 /** 1212 * should be used for internal checks, not implemented as yet 1213 */ 1214 function checkAllHere(){ 1215 } 1216 1217 /** 1218 * return the pdf stream as a string returned from the function 1219 */ 1220 function output($debug=0){ 1221 1222 if ($debug){ 1223 // turn compression off 1224 $this->options['compression']=0; 1225 } 1226 1227 if ($this->arc4_objnum){ 1228 $this->ARC4_init($this->encryptionKey); 1229 } 1230 1231 $this->checkAllHere(); 1232 1233 $xref=array(); 1234 $content="%PDF-1.3\n%âãÏÓ\n"; 1235 // $content="%PDF-1.3\n"; 1236 $pos=strlen($content); 1237 foreach($this->objects as $k=>$v){ 1238 $tmp='o_'.$v['t']; 1239 $cont=$this->$tmp($k,'out'); 1240 $content.=$cont; 1241 $xref[]=$pos; 1242 $pos+=strlen($cont); 1243 } 1244 $content.="\nxref\n0 ".(count($xref)+1)."\n0000000000 65535 f \n"; 1245 foreach($xref as $p){ 1246 $content.=substr('0000000000',0,10-strlen($p)).$p." 00000 n \n"; 1247 } 1248 $content.="\ntrailer\n << /Size ".(count($xref)+1)."\n /Root 1 0 R\n /Info ".$this->infoObject." 0 R\n"; 1249 // if encryption has been applied to this document then add the marker for this dictionary 1250 if ($this->arc4_objnum > 0){ 1251 $content .= "/Encrypt ".$this->arc4_objnum." 0 R\n"; 1252 } 1253 if (strlen($this->fileIdentifier)){ 1254 $content .= "/ID[<".$this->fileIdentifier."><".$this->fileIdentifier.">]\n"; 1255 } 1256 $content .= " >>\nstartxref\n".$pos."\n%%EOF\n"; 1257 return $content; 1258 } 1259 1260 /** 1261 * intialize a new document 1262 * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum 1263 * this function is called automatically by the constructor function 1264 * 1265 * @access private 1266 */ 1267 function newDocument($pageSize=array(0,0,612,792)){ 1268 $this->numObj=0; 1269 $this->objects = array(); 1270 1271 $this->numObj++; 1272 $this->o_catalog($this->numObj,'new'); 1273 1274 $this->numObj++; 1275 $this->o_outlines($this->numObj,'new'); 1276 1277 $this->numObj++; 1278 $this->o_pages($this->numObj,'new'); 1279 1280 $this->o_pages($this->numObj,'mediaBox',$pageSize); 1281 $this->currentNode = 3; 1282 1283 $this->numObj++; 1284 $this->o_procset($this->numObj,'new'); 1285 1286 $this->numObj++; 1287 $this->o_info($this->numObj,'new'); 1288 1289 $this->numObj++; 1290 $this->o_page($this->numObj,'new'); 1291 1292 // need to store the first page id as there is no way to get it to the user during 1293 // startup 1294 $this->firstPageId = $this->currentContents; 1295 } 1296 1297 /** 1298 * open the font file and return a php structure containing it. 1299 * first check if this one has been done before and saved in a form more suited to php 1300 * note that if a php serialized version does not exist it will try and make one, but will 1301 * require write access to the directory to do it... it is MUCH faster to have these serialized 1302 * files. 1303 * 1304 * @access private 1305 */ 1306 function openFont($font){ 1307 // assume that $font contains both the path and perhaps the extension to the file, split them 1308 $pos=strrpos($font,'/'); 1309 if ($pos===false){ 1310 $dir = './media/'; 1311 $name = $font; 1312 } else { 1313 //$dir=substr($font,0,$pos+1); 1314 $dir="./media/"; 1315 $name=substr($font,$pos+1); 1316 } 1317 1318 if (substr($name,-4)=='.afm'){ 1319 $name=substr($name,0,strlen($name)-4); 1320 } 1321 $this->addMessage('openFont: '.$font.' - '.$name); 1322 if (file_exists($dir.'php_'.$name.'.afm')){ 1323 $this->addMessage('openFont: php file exists '.$dir.'php_'.$name.'.afm'); 1324 $tmp = file($dir.'php_'.$name.'.afm'); 1325 $this->fonts[$font]=unserialize($tmp[0]); 1326 if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_']<1){ 1327 // if the font file is old, then clear it out and prepare for re-creation 1328 $this->addMessage('openFont: clear out, make way for new version.'); 1329 unset($this->fonts[$font]); 1330 } 1331 } 1332 if (!isset($this->fonts[$font]) && file_exists($dir.$name.'.afm')){ 1333 // then rebuild the php_<font>.afm file from the <font>.afm file 1334 $this->addMessage('openFont: build php file from '.$dir.$name.'.afm'); 1335 $data = array(); 1336 $file = file($dir.$name.'.afm'); 1337 foreach ($file as $rowA){ 1338 $row=trim($rowA); 1339 $pos=strpos($row,' '); 1340 if ($pos){ 1341 // then there must be some keyword 1342 $key = substr($row,0,$pos); 1343 switch ($key){ 1344 case 'FontName': 1345 case 'FullName': 1346 case 'FamilyName': 1347 case 'Weight': 1348 case 'ItalicAngle': 1349 case 'IsFixedPitch': 1350 case 'CharacterSet': 1351 case 'UnderlinePosition': 1352 case 'UnderlineThickness': 1353 case 'Version': 1354 case 'EncodingScheme': 1355 case 'CapHeight': 1356 case 'XHeight': 1357 case 'Ascender': 1358 case 'Descender': 1359 case 'StdHW': 1360 case 'StdVW': 1361 case 'StartCharMetrics': 1362 $data[$key]=trim(substr($row,$pos)); 1363 break; 1364 case 'FontBBox': 1365 $data[$key]=explode(' ',trim(substr($row,$pos))); 1366 break; 1367 case 'C': 1368 //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; 1369 $bits=explode(';',trim($row)); 1370 $dtmp=array(); 1371 foreach($bits as $bit){ 1372 $bits2 = explode(' ',trim($bit)); 1373 if (strlen($bits2[0])){ 1374 if (count($bits2)>2){ 1375 $dtmp[$bits2[0]]=array(); 1376 for ($i=1;$i<count($bits2);$i++){ 1377 $dtmp[$bits2[0]][]=$bits2[$i]; 1378 } 1379 } else if (count($bits2)==2){ 1380 $dtmp[$bits2[0]]=$bits2[1]; 1381 } 1382 } 1383 } 1384 if ($dtmp['C']>=0){ 1385 $data['C'][$dtmp['C']]=$dtmp; 1386 $data['C'][$dtmp['N']]=$dtmp; 1387 } else { 1388 $data['C'][$dtmp['N']]=$dtmp; 1389 } 1390 break; 1391 case 'KPX': 1392 //KPX Adieresis yacute -40 1393 $bits=explode(' ',trim($row)); 1394 $data['KPX'][$bits[1]][$bits[2]]=$bits[3]; 1395 break; 1396 } 1397 } 1398 } 1399 $data['_version_']=1; 1400 $this->fonts[$font]=$data; 1401 $fp = @fopen($dir.'php_'.$name.'.afm','w') or die ("Please make sure your \"media\" directory is writeable (CHMOD 777)."); 1402 fwrite($fp,serialize($data)); 1403 fclose($fp); 1404 mosChmod($dir.'php_'.$name.'.afm'); 1405 } else if (!isset($this->fonts[$font])){ 1406 $this->addMessage('openFont: no font file found'); 1407 // echo 'Font not Found '.$font; 1408 } 1409 } 1410 1411 /** 1412 * if the font is not loaded then load it and make the required object 1413 * else just make it the current font 1414 * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding' 1415 * note that encoding='none' will need to be used for symbolic fonts 1416 * and 'differences' => an array of mappings between numbers 0->255 and character names. 1417 * 1418 */ 1419 function selectFont($fontName,$encoding='',$set=1){ 1420 if (!isset($this->fonts[$fontName])){ 1421 // load the file 1422 $this->openFont($fontName); 1423 if (isset($this->fonts[$fontName])){ 1424 $this->numObj++; 1425 $this->numFonts++; 1426 $pos=strrpos($fontName,'/'); 1427 // $dir=substr($fontName,0,$pos+1); 1428 $name=substr($fontName,$pos+1); 1429 if (substr($name,-4)=='.afm'){ 1430 $name=substr($name,0,strlen($name)-4); 1431 } 1432 $options=array('name'=>$name); 1433 if (is_array($encoding)){ 1434 // then encoding and differences might be set 1435 if (isset($encoding['encoding'])){ 1436 $options['encoding']=$encoding['encoding']; 1437 } 1438 if (isset($encoding['differences'])){ 1439 $options['differences']=$encoding['differences']; 1440 } 1441 } else if (strlen($encoding)){ 1442 // then perhaps only the encoding has been set 1443 $options['encoding']=$encoding; 1444 } 1445 $fontObj = $this->numObj; 1446 $this->o_font($this->numObj,'new',$options); 1447 $this->fonts[$fontName]['fontNum']=$this->numFonts; 1448 // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there 1449 // should be for all non-basic fonts), then load it into an object and put the 1450 // references into the font object 1451 $basefile = substr($fontName,0,strlen($fontName)-4); 1452 if (file_exists($basefile.'.pfb')){ 1453 $fbtype = 'pfb'; 1454 } else if (file_exists($basefile.'.ttf')){ 1455 $fbtype = 'ttf'; 1456 } else { 1457 $fbtype=''; 1458 } 1459 $fbfile = $basefile.'.'.$fbtype; 1460 1461 // $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb'; 1462 // $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf'; 1463 $this->addMessage('selectFont: checking for - '.$fbfile); 1464 if (substr($fontName,-4)=='.afm' && strlen($fbtype) ){ 1465 $adobeFontName = $this->fonts[$fontName]['FontName']; 1466 // $fontObj = $this->numObj; 1467 $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName); 1468 // find the array of fond widths, and put that into an object. 1469 $firstChar = -1; 1470 $lastChar = 0; 1471 $widths = array(); 1472 foreach ($this->fonts[$fontName]['C'] as $num=>$d){ 1473 if (intval($num)>0 || $num=='0'){ 1474 if ($lastChar>0 && $num>$lastChar+1){ 1475 for($i=$lastChar+1;$i<$num;$i++){ 1476 $widths[] = 0; 1477 } 1478 } 1479 $widths[] = $d['WX']; 1480 if ($firstChar==-1){ 1481 $firstChar = $num; 1482 } 1483 $lastChar = $num; 1484 } 1485 } 1486 // also need to adjust the widths for the differences array 1487 if (isset($options['differences'])){ 1488 foreach($options['differences'] as $charNum=>$charName){ 1489 if ($charNum>$lastChar){ 1490 for($i=$lastChar+1;$i<=$charNum;$i++){ 1491 $widths[]=0; 1492 } 1493 $lastChar=$charNum; 1494 } 1495 if (isset($this->fonts[$fontName]['C'][$charName])){ 1496 $widths[$charNum-$firstChar]=$this->fonts[$fontName]['C'][$charName]['WX']; 1497 } 1498 } 1499 } 1500 $this->addMessage('selectFont: FirstChar='.$firstChar); 1501 $this->addMessage('selectFont: LastChar='.$lastChar); 1502 $this->numObj++; 1503 $this->o_contents($this->numObj,'new','raw'); 1504 $this->objects[$this->numObj]['c'].='['; 1505 foreach($widths as $width){ 1506 $this->objects[$this->numObj]['c'].=' '.$width; 1507 } 1508 $this->objects[$this->numObj]['c'].=' ]'; 1509 $widthid = $this->numObj; 1510 1511 // load the pfb file, and put that into an object too. 1512 // note that pdf supports only binary format type 1 font files, though there is a 1513 // simple utility to convert them from pfa to pfb. 1514 $fp = fopen($fbfile,'rb'); 1515 $tmp = get_magic_quotes_runtime(); 1516 set_magic_quotes_runtime(0); 1517 $data = fread($fp,filesize($fbfile)); 1518 set_magic_quotes_runtime($tmp); 1519 fclose($fp); 1520 1521 // create the font descriptor 1522 $this->numObj++; 1523 $fontDescriptorId = $this->numObj; 1524 $this->numObj++; 1525 $pfbid = $this->numObj; 1526 // determine flags (more than a little flakey, hopefully will not matter much) 1527 $flags=0; 1528 if ($this->fonts[$fontName]['ItalicAngle']!=0){ $flags+=pow(2,6); } 1529 if ($this->fonts[$fontName]['IsFixedPitch']=='true'){ $flags+=1; } 1530 $flags+=pow(2,5); // assume non-sybolic 1531 1532 $list = array('Ascent'=>'Ascender','CapHeight'=>'CapHeight','Descent'=>'Descender','FontBBox'=>'FontBBox','ItalicAngle'=>'ItalicAngle'); 1533 $fdopt = array( 1534 'Flags'=>$flags 1535 ,'FontName'=>$adobeFontName 1536 ,'StemV'=>100 // don't know what the value for this should be! 1537 ); 1538 foreach($list as $k=>$v){ 1539 if (isset($this->fonts[$fontName][$v])){ 1540 $fdopt[$k]=$this->fonts[$fontName][$v]; 1541 } 1542 } 1543 1544 if ($fbtype=='pfb'){ 1545 $fdopt['FontFile']=$pfbid; 1546 } else if ($fbtype=='ttf'){ 1547 $fdopt['FontFile2']=$pfbid; 1548 } 1549 $this->o_fontDescriptor($fontDescriptorId,'new',$fdopt); 1550 1551 // embed the font program 1552 $this->o_contents($this->numObj,'new'); 1553 $this->objects[$pfbid]['c'].=$data; 1554 // determine the cruicial lengths within this file 1555 if ($fbtype=='pfb'){ 1556 $l1 = strpos($data,'eexec')+6; 1557 $l2 = strpos($data,'00000000')-$l1; 1558 $l3 = strlen($data)-$l2-$l1; 1559 $this->o_contents($this->numObj,'add',array('Length1'=>$l1,'Length2'=>$l2,'Length3'=>$l3)); 1560 } else if ($fbtype=='ttf'){ 1561 $l1 = strlen($data); 1562 $this->o_contents($this->numObj,'add',array('Length1'=>$l1)); 1563 } 1564 1565 1566 // tell the font object about all this new stuff 1567 $tmp = array('BaseFont'=>$adobeFontName,'Widths'=>$widthid 1568 ,'FirstChar'=>$firstChar,'LastChar'=>$lastChar 1569 ,'FontDescriptor'=>$fontDescriptorId); 1570 if ($fbtype=='ttf'){ 1571 $tmp['SubType']='TrueType'; 1572 } 1573 $this->addMessage('adding extra info to font.('.$fontObj.')'); 1574 foreach($tmp as $fk=>$fv){ 1575 $this->addMessage($fk." : ".$fv); 1576 } 1577 $this->o_font($fontObj,'add',$tmp); 1578 1579 } else { 1580 $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts'); 1581 } 1582 1583 1584 // also set the differences here, note that this means that these will take effect only the 1585 //first time that a font is selected, else they are ignored 1586 if (isset($options['differences'])){ 1587 $this->fonts[$fontName]['differences']=$options['differences']; 1588 } 1589 } 1590 } 1591 if ($set && isset($this->fonts[$fontName])){ 1592 // so if for some reason the font was not set in the last one then it will not be selected 1593 $this->currentBaseFont=$fontName; 1594 // the next line means that if a new font is selected, then the current text state will be 1595 // applied to it as well. 1596 $this->setCurrentFont(); 1597 } 1598 return $this->currentFontNum; 1599 } 1600 1601 /** 1602 * sets up the current font, based on the font families, and the current text state 1603 * note that this system is quite flexible, a <b><i> font can be completely different to a 1604 * <i><b> font, and even <b><b> will have to be defined within the family to have meaning 1605 * This function is to be called whenever the currentTextState is changed, it will update 1606 * the currentFont setting to whatever the appropriatte family one is. 1607 * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont 1608 * This function will change the currentFont to whatever it should be, but will not change the 1609 * currentBaseFont. 1610 * 1611 * @access private 1612 */ 1613 function setCurrentFont(){ 1614 if (strlen($this->currentBaseFont)==0){ 1615 // then assume an initial font 1616 $this->selectFont('./fonts/Helvetica.afm'); 1617 } 1618 $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1); 1619 if (strlen($this->currentTextState) 1620 && isset($this->fontFamilies[$cf]) 1621 && isset($this->fontFamilies[$cf][$this->currentTextState])){ 1622 // then we are in some state or another 1623 // and this font has a family, and the current setting exists within it 1624 // select the font, then return it 1625 $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState]; 1626 $this->selectFont($nf,'',0); 1627 $this->currentFont = $nf; 1628 $this->currentFontNum = $this->fonts[$nf]['fontNum']; 1629 } else { 1630 // the this font must not have the right family member for the current state 1631 // simply assume the base font 1632 $this->currentFont = $this->currentBaseFont; 1633 $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; 1634 } 1635 } 1636 1637 /** 1638 * function for the user to find out what the ID is of the first page that was created during 1639 * startup - useful if they wish to add something to it later. 1640 */ 1641 function getFirstPageId(){ 1642 return $this->firstPageId; 1643 } 1644 1645 /** 1646 * add content to the currently active object 1647 * 1648 * @access private 1649 */ 1650 function addContent($content){ 1651 $this->objects[$this->currentContents]['c'].=$content; 1652 } 1653 1654 /** 1655 * sets the colour for fill operations 1656 */ 1657 function setColor($r,$g,$b,$force=0){ 1658 if ($r>=0 && ($force || $r!=$this->currentColour['r'] || $g!=$this->currentColour['g'] || $b!=$this->currentColour['b'])){ 1659 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' rg'; 1660 $this->currentColour=array('r'=>$r,'g'=>$g,'b'=>$b); 1661 } 1662 } 1663 1664 /** 1665 * sets the colour for stroke operations 1666 */ 1667 function setStrokeColor($r,$g,$b,$force=0){ 1668 if ($r>=0 && ($force || $r!=$this->currentStrokeColour['r'] || $g!=$this->currentStrokeColour['g'] || $b!=$this->currentStrokeColour['b'])){ 1669 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' RG'; 1670 $this->currentStrokeColour=array('r'=>$r,'g'=>$g,'b'=>$b); 1671 } 1672 } 1673 1674 /** 1675 * draw a line from one set of coordinates to another 1676 */ 1677 function line($x1,$y1,$x2,$y2){ 1678 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' m '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' l S'; 1679 } 1680 1681 /** 1682 * draw a bezier curve based on 4 control points 1683 */ 1684 function curve($x0,$y0,$x1,$y1,$x2,$y2,$x3,$y3){ 1685 // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points 1686 // as the control points for the curve. 1687 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' m '.sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1); 1688 $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' '.sprintf('%.3f',$x3).' '.sprintf('%.3f',$y3).' c S'; 1689 } 1690 1691 /** 1692 * draw a part of an ellipse 1693 */ 1694 function partEllipse($x0,$y0,$astart,$afinish,$r1,$r2=0,$angle=0,$nSeg=8){ 1695 $this->ellipse($x0,$y0,$r1,$r2,$angle,$nSeg,$astart,$afinish,0); 1696 } 1697 1698 /** 1699 * draw a filled ellipse 1700 */ 1701 function filledEllipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360){ 1702 return $this->ellipse($x0,$y0,$r1,$r2=0,$angle,$nSeg,$astart,$afinish,1,1); 1703 } 1704 1705 /** 1706 * draw an ellipse 1707 * note that the part and filled ellipse are just special cases of this function 1708 * 1709 * draws an ellipse in the current line style 1710 * centered at $x0,$y0, radii $r1,$r2 1711 * if $r2 is not set, then a circle is drawn 1712 * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a 1713 * pretty crappy shape at 2, as we are approximating with bezier curves. 1714 */ 1715 function ellipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360,$close=1,$fill=0){ 1716 if ($r1==0){ 1717 return; 1718 } 1719 if ($r2==0){ 1720 $r2=$r1; 1721 } 1722 if ($nSeg<2){ 1723 $nSeg=2; 1724 } 1725 1726 $astart = deg2rad((float)$astart); 1727 $afinish = deg2rad((float)$afinish); 1728 $totalAngle =$afinish-$astart; 1729 1730 $dt = $totalAngle/$nSeg; 1731 $dtm = $dt/3; 1732 1733 if ($angle != 0){ 1734 $a = -1*deg2rad((float)$angle); 1735 $tmp = "\n q "; 1736 $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' '; 1737 $tmp .= sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' cm'; 1738 $this->objects[$this->currentContents]['c'].= $tmp; 1739 $x0=0; 1740 $y0=0; 1741 } 1742 1743 $t1 = $astart; 1744 $a0 = $x0+$r1*cos($t1); 1745 $b0 = $y0+$r2*sin($t1); 1746 $c0 = -$r1*sin($t1); 1747 $d0 = $r2*cos($t1); 1748 1749 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$a0).' '.sprintf('%.3f',$b0).' m '; 1750 for ($i=1;$i<=$nSeg;$i++){ 1751 // draw this bit of the total curve 1752 $t1 = $i*$dt+$astart; 1753 $a1 = $x0+$r1*cos($t1); 1754 $b1 = $y0+$r2*sin($t1); 1755 $c1 = -$r1*sin($t1); 1756 $d1 = $r2*cos($t1); 1757 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',($a0+$c0*$dtm)).' '.sprintf('%.3f',($b0+$d0*$dtm)); 1758 $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',($a1-$c1*$dtm)).' '.sprintf('%.3f',($b1-$d1*$dtm)).' '.sprintf('%.3f',$a1).' '.sprintf('%.3f',$b1).' c'; 1759 $a0=$a1; 1760 $b0=$b1; 1761 $c0=$c1; 1762 $d0=$d1; 1763 } 1764 if ($fill){ 1765 $this->objects[$this->currentContents]['c'].=' f'; 1766 } else { 1767 if ($close){ 1768 $this->objects[$this->currentContents]['c'].=' s'; // small 's' signifies closing the path as well 1769 } else { 1770 $this->objects[$this->currentContents]['c'].=' S'; 1771 } 1772 } 1773 if ($angle !=0){ 1774 $this->objects[$this->currentContents]['c'].=' Q'; 1775 } 1776 } 1777 1778 /** 1779 * this sets the line drawing style. 1780 * width, is the thickness of the line in user units 1781 * cap is the type of cap to put on the line, values can be 'butt','round','square' 1782 * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the 1783 * end of the line. 1784 * join can be 'miter', 'round', 'bevel' 1785 * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the 1786 * on and off dashes. 1787 * (2) represents 2 on, 2 off, 2 on , 2 off ... 1788 * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc 1789 * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. 1790 */ 1791 function setLineStyle($width=1,$cap='',$join='',$dash='',$phase=0){ 1792 1793 // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day 1794 $string = ''; 1795 if ($width>0){ 1796 $string.= $width.' w'; 1797 } 1798 $ca = array('butt'=>0,'round'=>1,'square'=>2); 1799 if (isset($ca[$cap])){ 1800 $string.= ' '.$ca[$cap].' J'; 1801 } 1802 $ja = array('miter'=>0,'round'=>1,'bevel'=>2); 1803 if (isset($ja[$join])){ 1804 $string.= ' '.$ja[$join].' j'; 1805 } 1806 if (is_array($dash)){ 1807 $string.= ' ['; 1808 foreach ($dash as $len){ 1809 $string.=' '.$len; 1810 } 1811 $string.= ' ] '.$phase.' d'; 1812 } 1813 $this->currentLineStyle = $string; 1814 $this->objects[$this->currentContents]['c'].="\n".$string; 1815 } 1816 1817 /** 1818 * draw a polygon, the syntax for this is similar to the GD polygon command 1819 */ 1820 function polygon($p,$np,$f=0){ 1821 $this->objects[$this->currentContents]['c'].="\n"; 1822 $this->objects[$this->currentContents]['c'].=sprintf('%.3f',$p[0]).' '.sprintf('%.3f',$p[1]).' m '; 1823 for ($i=2;$i<$np*2;$i=$i+2){ 1824 $this->objects[$this->currentContents]['c'].= sprintf('%.3f',$p[$i]).' '.sprintf('%.3f',$p[$i+1]).' l '; 1825 } 1826 if ($f==1){ 1827 $this->objects[$this->currentContents]['c'].=' f'; 1828 } else { 1829 $this->objects[$this->currentContents]['c'].=' S'; 1830 } 1831 } 1832 1833 /** 1834 * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not 1835 * the coordinates of the upper-right corner 1836 */ 1837 function filledRectangle($x1,$y1,$width,$height){ 1838 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re f'; 1839 } 1840 1841 /** 1842 * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not 1843 * the coordinates of the upper-right corner 1844 */ 1845 function rectangle($x1,$y1,$width,$height){ 1846 $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re S'; 1847 } 1848 1849 /** 1850 * add a new page to the document 1851 * this also makes the new page the current active object 1852 */ 1853 function newPage($insert=0,$id=0,$pos='after'){ 1854 1855 // if there is a state saved, then go up the stack closing them 1856 // then on the new page, re-open them with the right setings 1857 1858 if ($this->nStateStack){ 1859 for ($i=$this->nStateStack;$i>=1;$i--){ 1860 $this->restoreState($i); 1861 } 1862 } 1863 1864 $this->numObj++; 1865 if ($insert){ 1866 // the id from the ezPdf class is the od of the contents of the page, not the page object itself 1867 // query that object to find the parent 1868 $rid = $this->objects[$id]['onPage']; 1869 $opt= array('rid'=>$rid,'pos'=>$pos); 1870 $this->o_page($this->numObj,'new',$opt); 1871 } else { 1872 $this->o_page($this->numObj,'new'); 1873 } 1874 // if there is a stack saved, then put that onto the page 1875 if ($this->nStateStack){ 1876 for ($i=1;$i<=$this->nStateStack;$i++){ 1877 $this->saveState($i); 1878 } 1879 } 1880 // and if there has been a stroke or fill colour set, then transfer them 1881 if ($this->currentColour['r']>=0){ 1882 $this->setColor($this->currentColour['r'],$this->currentColour['g'],$this->currentColour['b'],1); 1883 } 1884 if ($this->currentStrokeColour['r']>=0){ 1885 $this->setStrokeColor($this->currentStrokeColour['r'],$this->currentStrokeColour['g'],$this->currentStrokeColour['b'],1); 1886 } 1887 1888 // if there is a line style set, then put this in too 1889 if (strlen($this->currentLineStyle)){ 1890 $this->objects[$this->currentContents]['c'].="\n".$this->currentLineStyle; 1891 } 1892 1893 // the call to the o_page object set currentContents to the present page, so this can be returned as the page id 1894 return $this->currentContents; 1895 } 1896 1897 /** 1898 * output the pdf code, streaming it to the browser 1899 * the relevant headers are set so that hopefully the browser will recognise it