/lib/class.pdf.php
PHP | 4607 lines | 2833 code | 736 blank | 1038 comment | 554 complexity | ed7e198371529c7aa5db09e0d9b921e0 MD5 | raw file
1<?php 2/** 3 * A PHP class to provide the basic functionality to create a pdf document without 4 * any requirement for additional modules. 5 * 6 * Extended by Orion Richardson to support Unicode / UTF-8 characters using 7 * TCPDF and others as a guide. 8 * 9 * @author Wayne Munro <pdf@ros.co.nz> 10 * @author Orion Richardson <orionr@yahoo.com> 11 * @author Helmut Tischer <htischer@weihenstephan.org> 12 * @author Ryan H. Masten <ryan.masten@gmail.com> 13 * @author Brian Sweeney <eclecticgeek@gmail.com> 14 * @author Fabien MĂŠnager <fabien.menager@gmail.com> 15 * @license Public Domain http://creativecommons.org/licenses/publicdomain/ 16 * @package Cpdf 17 */ 18class Cpdf { 19 20 /** 21 * @var integer The current number of pdf objects in the document 22 */ 23 public $numObj = 0; 24 25 /** 26 * @var array This array contains all of the pdf objects, ready for final assembly 27 */ 28 public $objects = array(); 29 30 /** 31 * @var integer The objectId (number within the objects array) of the document catalog 32 */ 33 public $catalogId; 34 35 /** 36 * @var array Array carrying information about the fonts that the system currently knows about 37 * Used to ensure that a font is not loaded twice, among other things 38 */ 39 public $fonts = array(); 40 41 /** 42 * @var string The default font metrics file to use if no other font has been loaded. 43 * The path to the directory containing the font metrics should be included 44 */ 45 public $defaultFont = './fonts/Helvetica.afm'; 46 47 /** 48 * @string A record of the current font 49 */ 50 public $currentFont = ''; 51 52 /** 53 * @var string The current base font 54 */ 55 public $currentBaseFont = ''; 56 57 /** 58 * @var integer The number of the current font within the font array 59 */ 60 public $currentFontNum = 0; 61 62 /** 63 * @var integer 64 */ 65 public $currentNode; 66 67 /** 68 * @var integer Object number of the current page 69 */ 70 public $currentPage; 71 72 /** 73 * @var integer Object number of the currently active contents block 74 */ 75 public $currentContents; 76 77 /** 78 * @var integer Number of fonts within the system 79 */ 80 public $numFonts = 0; 81 82 /** 83 * @var integer Number of graphic state resources used 84 */ 85 private $numStates = 0; 86 87 /** 88 * @var array Current color for fill operations, defaults to inactive value, 89 * all three components should be between 0 and 1 inclusive when active 90 */ 91 public $currentColor = null; 92 93 /** 94 * @var array Current color for stroke operations (lines etc.) 95 */ 96 public $currentStrokeColor = null; 97 98 /** 99 * @var string Current style that lines are drawn in 100 */ 101 public $currentLineStyle = ''; 102 103 /** 104 * @var array Current line transparency (partial graphics state) 105 */ 106 public $currentLineTransparency = array("mode" => "Normal", "opacity" => 1.0); 107 108 /** 109 * array Current fill transparency (partial graphics state) 110 */ 111 public $currentFillTransparency = array("mode" => "Normal", "opacity" => 1.0); 112 113 /** 114 * @var array An array which is used to save the state of the document, mainly the colors and styles 115 * it is used to temporarily change to another state, the change back to what it was before 116 */ 117 public $stateStack = array(); 118 119 /** 120 * @var integer Number of elements within the state stack 121 */ 122 public $nStateStack = 0; 123 124 /** 125 * @var integer Number of page objects within the document 126 */ 127 public $numPages = 0; 128 129 /** 130 * @var array Object Id storage stack 131 */ 132 public $stack = array(); 133 134 /** 135 * @var integer Number of elements within the object Id storage stack 136 */ 137 public $nStack = 0; 138 139 /** 140 * an array which contains information about the objects which are not firmly attached to pages 141 * these have been added with the addObject function 142 */ 143 public $looseObjects = array(); 144 145 /** 146 * array contains infomation about how the loose objects are to be added to the document 147 */ 148 public $addLooseObjects = array(); 149 150 /** 151 * @var integer The objectId of the information object for the document 152 * this contains authorship, title etc. 153 */ 154 public $infoObject = 0; 155 156 /** 157 * @var integer Number of images being tracked within the document 158 */ 159 public $numImages = 0; 160 161 /** 162 * @var array An array containing options about the document 163 * it defaults to turning on the compression of the objects 164 */ 165 public $options = array('compression' => true); 166 167 /** 168 * @var integer The objectId of the first page of the document 169 */ 170 public $firstPageId; 171 172 /** 173 * @var float Used to track the last used value of the inter-word spacing, this is so that it is known 174 * when the spacing is changed. 175 */ 176 public $wordSpaceAdjust = 0; 177 178 /** 179 * @var float Used to track the last used value of the inter-letter spacing, this is so that it is known 180 * when the spacing is changed. 181 */ 182 public $charSpaceAdjust = 0; 183 184 /** 185 * @var integer The object Id of the procset object 186 */ 187 public $procsetObjectId; 188 189 /** 190 * @var array Store the information about the relationship between font families 191 * this used so that the code knows which font is the bold version of another font, etc. 192 * the value of this array is initialised in the constuctor function. 193 */ 194 public $fontFamilies = array(); 195 196 /** 197 * @var string Folder for php serialized formats of font metrics files. 198 * If empty string, use same folder as original metrics files. 199 * This can be passed in from class creator. 200 * If this folder does not exist or is not writable, Cpdf will be **much** slower. 201 * Because of potential trouble with php safe mode, folder cannot be created at runtime. 202 */ 203 public $fontcache = ''; 204 205 /** 206 * @var integer The version of the font metrics cache file. 207 * This value must be manually incremented whenever the internal font data structure is modified. 208 */ 209 public $fontcacheVersion = 6; 210 211 /** 212 * @var string Temporary folder. 213 * If empty string, will attempty system tmp folder. 214 * This can be passed in from class creator. 215 * Only used for conversion of gd images to jpeg images. 216 */ 217 public $tmp = ''; 218 219 /** 220 * @var string Track if the current font is bolded or italicised 221 */ 222 public $currentTextState = ''; 223 224 /** 225 * @var string Messages are stored here during processing, these can be selected afterwards to give some useful debug information 226 */ 227 public $messages = ''; 228 229 /** 230 * @var string The ancryption array for the document encryption is stored here 231 */ 232 public $arc4 = ''; 233 234 /** 235 * @var integer The object Id of the encryption information 236 */ 237 public $arc4_objnum = 0; 238 239 /** 240 * @var string The file identifier, used to uniquely identify a pdf document 241 */ 242 public $fileIdentifier = ''; 243 244 /** 245 * @var boolean A flag to say if a document is to be encrypted or not 246 */ 247 public $encrypted = false; 248 249 /** 250 * @var string The encryption key for the encryption of all the document content (structure is not encrypted) 251 */ 252 public $encryptionKey = ''; 253 254 /** 255 * @var array Array which forms a stack to keep track of nested callback functions 256 */ 257 public $callback = array(); 258 259 /** 260 * @var integer The number of callback functions in the callback array 261 */ 262 public $nCallback = 0; 263 264 /** 265 * @var array Store label->id pairs for named destinations, these will be used to replace internal links 266 * done this way so that destinations can be defined after the location that links to them 267 */ 268 public $destinations = array(); 269 270 /** 271 * @var array Store the stack for the transaction commands, each item in here is a record of the values of all the 272 * publiciables within the class, so that the user can rollback at will (from each 'start' command) 273 * note that this includes the objects array, so these can be large. 274 */ 275 public $checkpoint = ''; 276 277 /** 278 * @var array Table of Image origin filenames and image labels which were already added with o_image(). 279 * Allows to merge identical images 280 */ 281 public $imagelist = array(); 282 283 /** 284 * @var boolean Whether the text passed in should be treated as Unicode or just local character set. 285 */ 286 public $isUnicode = false; 287 288 /** 289 * @var string the JavaScript code of the document 290 */ 291 public $javascript = ''; 292 293 /** 294 * @var boolean whether the compression is possible 295 */ 296 protected $compressionReady = false; 297 298 /** 299 * @var array Current page size 300 */ 301 protected $currentPageSize = array("width" => 0, "height" => 0); 302 303 /** 304 * @var array All the chars that will be required in the font subsets 305 */ 306 protected $stringSubsets = array(); 307 308 /** 309 * @var string The target internal encoding 310 */ 311 static protected $targetEncoding = 'iso-8859-1'; 312 313 /** 314 * @var array The list of the core fonts 315 */ 316 static protected $coreFonts = array( 317 'courier', 'courier-bold', 'courier-oblique', 'courier-boldoblique', 318 'helvetica', 'helvetica-bold', 'helvetica-oblique', 'helvetica-boldoblique', 319 'times-roman', 'times-bold', 'times-italic', 'times-bolditalic', 320 'symbol', 'zapfdingbats' 321 ); 322 323 /** 324 * Class constructor 325 * This will start a new document 326 * 327 * @param array $pageSize Array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero. 328 * @param boolean $isUnicode Whether text will be treated as Unicode or not. 329 * @param string $fontcache The font cache folder 330 * @param string $tmp The temporary folder 331 */ 332 function __construct($pageSize = array(0, 0, 612, 792), $isUnicode = false, $fontcache = '', $tmp = '') { 333 $this->isUnicode = $isUnicode; 334 $this->fontcache = $fontcache; 335 $this->tmp = $tmp; 336 $this->newDocument($pageSize); 337 338 $this->compressionReady = function_exists('gzcompress'); 339 340 if ( in_array('Windows-1252', mb_list_encodings()) ) { 341 self::$targetEncoding = 'Windows-1252'; 342 } 343 344 // also initialize the font families that are known about already 345 $this->setFontFamily('init'); 346 // $this->fileIdentifier = md5('xxxxxxxx'.time()); 347 } 348 349 /** 350 * Document object methods (internal use only) 351 * 352 * There is about one object method for each type of object in the pdf document 353 * Each function has the same call list ($id,$action,$options). 354 * $id = the object ID of the object, or what it is to be if it is being created 355 * $action = a string specifying the action to be performed, though ALL must support: 356 * 'new' - create the object with the id $id 357 * 'out' - produce the output for the pdf object 358 * $options = optional, a string or array containing the various parameters for the object 359 * 360 * These, in conjunction with the output function are the ONLY way for output to be produced 361 * within the pdf 'file'. 362 */ 363 364 /** 365 * Destination object, used to specify the location for the user to jump to, presently on opening 366 */ 367 protected function o_destination($id, $action, $options = '') { 368 if ($action !== 'new') { 369 $o = &$this->objects[$id]; 370 } 371 372 switch ($action) { 373 case 'new': 374 $this->objects[$id] = array('t' => 'destination', 'info' => array()); 375 $tmp = ''; 376 switch ($options['type']) { 377 case 'XYZ': 378 case 'FitR': 379 $tmp = ' '.$options['p3'].$tmp; 380 case 'FitH': 381 case 'FitV': 382 case 'FitBH': 383 case 'FitBV': 384 $tmp = ' '.$options['p1'].' '.$options['p2'].$tmp; 385 case 'Fit': 386 case 'FitB': 387 $tmp = $options['type'].$tmp; 388 $this->objects[$id]['info']['string'] = $tmp; 389 $this->objects[$id]['info']['page'] = $options['page']; 390 } 391 break; 392 393 case 'out': 394 $tmp = $o['info']; 395 $res = "\n$id 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj"; 396 return $res; 397 } 398 } 399 400 /** 401 * set the viewer preferences 402 */ 403 protected function o_viewerPreferences($id, $action, $options = '') { 404 if ($action !== 'new') { 405 $o = & $this->objects[$id]; 406 } 407 408 switch ($action) { 409 case 'new': 410 $this->objects[$id] = array('t' => 'viewerPreferences', 'info' => array()); 411 break; 412 413 case 'add': 414 foreach ($options as $k => $v) { 415 switch ($k) { 416 case 'HideToolbar': 417 case 'HideMenubar': 418 case 'HideWindowUI': 419 case 'FitWindow': 420 case 'CenterWindow': 421 case 'NonFullScreenPageMode': 422 case 'Direction': 423 $o['info'][$k] = $v; 424 break; 425 } 426 } 427 break; 428 429 case 'out': 430 $res = "\n$id 0 obj\n<< "; 431 foreach ($o['info'] as $k => $v) { 432 $res.= "\n/$k $v"; 433 } 434 $res.= "\n>>\n"; 435 return $res; 436 } 437 } 438 439 /** 440 * define the document catalog, the overall controller for the document 441 */ 442 protected function o_catalog($id, $action, $options = '') { 443 if ($action !== 'new') { 444 $o = & $this->objects[$id]; 445 } 446 447 switch ($action) { 448 case 'new': 449 $this->objects[$id] = array('t' => 'catalog', 'info' => array()); 450 $this->catalogId = $id; 451 break; 452 453 case 'outlines': 454 case 'pages': 455 case 'openHere': 456 case 'javascript': 457 $o['info'][$action] = $options; 458 break; 459 460 case 'viewerPreferences': 461 if (!isset($o['info']['viewerPreferences'])) { 462 $this->numObj++; 463 $this->o_viewerPreferences($this->numObj, 'new'); 464 $o['info']['viewerPreferences'] = $this->numObj; 465 } 466 467 $vp = $o['info']['viewerPreferences']; 468 $this->o_viewerPreferences($vp, 'add', $options); 469 470 break; 471 472 case 'out': 473 $res = "\n$id 0 obj\n<< /Type /Catalog"; 474 475 foreach ($o['info'] as $k => $v) { 476 switch ($k) { 477 case 'outlines': 478 $res.= "\n/Outlines $v 0 R"; 479 break; 480 481 case 'pages': 482 $res.= "\n/Pages $v 0 R"; 483 break; 484 485 case 'viewerPreferences': 486 $res.= "\n/ViewerPreferences $v 0 R"; 487 break; 488 489 case 'openHere': 490 $res.= "\n/OpenAction $v 0 R"; 491 break; 492 493 case 'javascript': 494 $res.= "\n/Names <</JavaScript $v 0 R>>"; 495 break; 496 } 497 } 498 499 $res.= " >>\nendobj"; 500 return $res; 501 } 502 } 503 504 /** 505 * object which is a parent to the pages in the document 506 */ 507 protected function o_pages($id, $action, $options = '') { 508 if ($action !== 'new') { 509 $o = & $this->objects[$id]; 510 } 511 512 switch ($action) { 513 case 'new': 514 $this->objects[$id] = array('t' => 'pages', 'info' => array()); 515 $this->o_catalog($this->catalogId, 'pages', $id); 516 break; 517 518 case 'page': 519 if (!is_array($options)) { 520 // then it will just be the id of the new page 521 $o['info']['pages'][] = $options; 522 } 523 else { 524 // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative 525 // and pos is either 'before' or 'after', saying where this page will fit. 526 if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])) { 527 $i = array_search($options['rid'], $o['info']['pages']); 528 if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i] == $options['rid']) { 529 530 // then there is a match 531 // make a space 532 switch ($options['pos']) { 533 case 'before': 534 $k = $i; 535 break; 536 537 case 'after': 538 $k = $i+1; 539 break; 540 541 default: 542 $k = -1; 543 break; 544 } 545 546 if ($k >= 0) { 547 for ($j = count($o['info']['pages'])-1; $j >= $k; $j--) { 548 $o['info']['pages'][$j+1] = $o['info']['pages'][$j]; 549 } 550 551 $o['info']['pages'][$k] = $options['id']; 552 } 553 } 554 } 555 } 556 break; 557 558 case 'procset': 559 $o['info']['procset'] = $options; 560 break; 561 562 case 'mediaBox': 563 $o['info']['mediaBox'] = $options; 564 // which should be an array of 4 numbers 565 $this->currentPageSize = array('width' => $options[2], 'height' => $options[3]); 566 break; 567 568 case 'font': 569 $o['info']['fonts'][] = array('objNum' => $options['objNum'], 'fontNum' => $options['fontNum']); 570 break; 571 572 case 'extGState': 573 $o['info']['extGStates'][] = array('objNum' => $options['objNum'], 'stateNum' => $options['stateNum']); 574 break; 575 576 case 'xObject': 577 $o['info']['xObjects'][] = array('objNum' => $options['objNum'], 'label' => $options['label']); 578 break; 579 580 case 'out': 581 if (count($o['info']['pages'])) { 582 $res = "\n$id 0 obj\n<< /Type /Pages\n/Kids ["; 583 foreach ($o['info']['pages'] as $v) { 584 $res.= "$v 0 R\n"; 585 } 586 587 $res.= "]\n/Count ".count($this->objects[$id]['info']['pages']); 588 589 if ( (isset($o['info']['fonts']) && count($o['info']['fonts'])) || 590 isset($o['info']['procset']) || 591 (isset($o['info']['extGStates']) && count($o['info']['extGStates']))) { 592 $res.= "\n/Resources <<"; 593 594 if (isset($o['info']['procset'])) { 595 $res.= "\n/ProcSet ".$o['info']['procset']." 0 R"; 596 } 597 598 if (isset($o['info']['fonts']) && count($o['info']['fonts'])) { 599 $res.= "\n/Font << "; 600 foreach ($o['info']['fonts'] as $finfo) { 601 $res.= "\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R"; 602 } 603 $res.= "\n>>"; 604 } 605 606 if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])) { 607 $res.= "\n/XObject << "; 608 foreach ($o['info']['xObjects'] as $finfo) { 609 $res.= "\n/".$finfo['label']." ".$finfo['objNum']." 0 R"; 610 } 611 $res.= "\n>>"; 612 } 613 614 if ( isset($o['info']['extGStates']) && count($o['info']['extGStates'])) { 615 $res.= "\n/ExtGState << "; 616 foreach ($o['info']['extGStates'] as $gstate) { 617 $res.= "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R"; 618 } 619 $res.= "\n>>"; 620 } 621 622 $res.= "\n>>"; 623 if (isset($o['info']['mediaBox'])) { 624 $tmp = $o['info']['mediaBox']; 625 $res.= "\n/MediaBox [".sprintf('%.3F %.3F %.3F %.3F', $tmp[0], $tmp[1], $tmp[2], $tmp[3]) .']'; 626 } 627 } 628 629 $res.= "\n >>\nendobj"; 630 } 631 else { 632 $res = "\n$id 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj"; 633 } 634 635 return $res; 636 } 637 } 638 639 /** 640 * define the outlines in the doc, empty for now 641 */ 642 protected function o_outlines($id, $action, $options = '') { 643 if ($action !== 'new') { 644 $o = &$this->objects[$id]; 645 } 646 647 switch ($action) { 648 case 'new': 649 $this->objects[$id] = array('t' => 'outlines', 'info' => array('outlines' => array())); 650 $this->o_catalog($this->catalogId, 'outlines', $id); 651 break; 652 653 case 'outline': 654 $o['info']['outlines'][] = $options; 655 break; 656 657 case 'out': 658 if (count($o['info']['outlines'])) { 659 $res = "\n$id 0 obj\n<< /Type /Outlines /Kids ["; 660 foreach ($o['info']['outlines'] as $v) { 661 $res.= "$v 0 R "; 662 } 663 664 $res.= "] /Count ".count($o['info']['outlines']) ." >>\nendobj"; 665 } else { 666 $res = "\n$id 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj"; 667 } 668 669 return $res; 670 } 671 } 672 673 /** 674 * an object to hold the font description 675 */ 676 protected function o_font($id, $action, $options = '') { 677 if ($action !== 'new') { 678 $o = &$this->objects[$id]; 679 } 680 681 switch ($action) { 682 case 'new': 683 $this->objects[$id] = array('t' => 'font', 'info' => array('name' => $options['name'], 'fontFileName' => $options['fontFileName'], 'SubType' => 'Type1')); 684 $fontNum = $this->numFonts; 685 $this->objects[$id]['info']['fontNum'] = $fontNum; 686 687 // deal with the encoding and the differences 688 if (isset($options['differences'])) { 689 // then we'll need an encoding dictionary 690 $this->numObj++; 691 $this->o_fontEncoding($this->numObj, 'new', $options); 692 $this->objects[$id]['info']['encodingDictionary'] = $this->numObj; 693 } 694 else if (isset($options['encoding'])) { 695 // we can specify encoding here 696 switch ($options['encoding']) { 697 case 'WinAnsiEncoding': 698 case 'MacRomanEncoding': 699 case 'MacExpertEncoding': 700 $this->objects[$id]['info']['encoding'] = $options['encoding']; 701 break; 702 703 case 'none': 704 break; 705 706 default: 707 $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; 708 break; 709 } 710 } 711 else { 712 $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; 713 } 714 715 if ($this->fonts[$options['fontFileName']]['isUnicode']) { 716 // For Unicode fonts, we need to incorporate font data into 717 // sub-sections that are linked from the primary font section. 718 // Look at o_fontGIDtoCID and o_fontDescendentCID functions 719 // for more informaiton. 720 // 721 // All of this code is adapted from the excellent changes made to 722 // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) 723 724 $toUnicodeId = ++$this->numObj; 725 $this->o_contents($toUnicodeId, 'new', 'raw'); 726 $this->objects[$id]['info']['toUnicode'] = $toUnicodeId; 727 728 $stream = <<<EOT 729/CIDInit /ProcSet findresource begin 73012 dict begin 731begincmap 732/CIDSystemInfo 733<</Registry (Adobe) 734/Ordering (UCS) 735/Supplement 0 736>> def 737/CMapName /Adobe-Identity-UCS def 738/CMapType 2 def 7391 begincodespacerange 740<0000> <FFFF> 741endcodespacerange 7421 beginbfrange 743<0000> <FFFF> <0000> 744endbfrange 745endcmap 746CMapName currentdict /CMap defineresource pop 747end 748end 749EOT; 750 751 $res = "<</Length " . mb_strlen($stream, '8bit') . " >>\n"; 752 $res .= "stream\n" . $stream . "endstream"; 753 754 $this->objects[$toUnicodeId]['c'] = $res; 755 756 $cidFontId = ++$this->numObj; 757 $this->o_fontDescendentCID($cidFontId, 'new', $options); 758 $this->objects[$id]['info']['cidFont'] = $cidFontId; 759 } 760 761 // also tell the pages node about the new font 762 $this->o_pages($this->currentNode, 'font', array('fontNum' => $fontNum, 'objNum' => $id)); 763 break; 764 765 case 'add': 766 foreach ($options as $k => $v) { 767 switch ($k) { 768 case 'BaseFont': 769 $o['info']['name'] = $v; 770 break; 771 case 'FirstChar': 772 case 'LastChar': 773 case 'Widths': 774 case 'FontDescriptor': 775 case 'SubType': 776 $this->addMessage('o_font '.$k." : ".$v); 777 $o['info'][$k] = $v; 778 break; 779 } 780 } 781 782 // pass values down to descendent font 783 if (isset($o['info']['cidFont'])) { 784 $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $options); 785 } 786 break; 787 788 case 'out': 789 if ($this->fonts[$this->objects[$id]['info']['fontFileName']]['isUnicode']) { 790 // For Unicode fonts, we need to incorporate font data into 791 // sub-sections that are linked from the primary font section. 792 // Look at o_fontGIDtoCID and o_fontDescendentCID functions 793 // for more informaiton. 794 // 795 // All of this code is adapted from the excellent changes made to 796 // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) 797 798 $res = "\n$id 0 obj\n<</Type /Font\n/Subtype /Type0\n"; 799 $res.= "/BaseFont /".$o['info']['name']."\n"; 800 801 // The horizontal identity mapping for 2-byte CIDs; may be used 802 // with CIDFonts using any Registry, Ordering, and Supplement values. 803 $res.= "/Encoding /Identity-H\n"; 804 $res.= "/DescendantFonts [".$o['info']['cidFont']." 0 R]\n"; 805 $res.= "/ToUnicode ".$o['info']['toUnicode']." 0 R\n"; 806 $res.= ">>\n"; 807 $res.= "endobj"; 808 } else { 809 $res = "\n$id 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n"; 810 $res.= "/Name /F".$o['info']['fontNum']."\n"; 811 $res.= "/BaseFont /".$o['info']['name']."\n"; 812 813 if (isset($o['info']['encodingDictionary'])) { 814 // then place a reference to the dictionary 815 $res.= "/Encoding ".$o['info']['encodingDictionary']." 0 R\n"; 816 } else if (isset($o['info']['encoding'])) { 817 // use the specified encoding 818 $res.= "/Encoding /".$o['info']['encoding']."\n"; 819 } 820 821 if (isset($o['info']['FirstChar'])) { 822 $res.= "/FirstChar ".$o['info']['FirstChar']."\n"; 823 } 824 825 if (isset($o['info']['LastChar'])) { 826 $res.= "/LastChar ".$o['info']['LastChar']."\n"; 827 } 828 829 if (isset($o['info']['Widths'])) { 830 $res.= "/Widths ".$o['info']['Widths']." 0 R\n"; 831 } 832 833 if (isset($o['info']['FontDescriptor'])) { 834 $res.= "/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n"; 835 } 836 837 $res.= ">>\n"; 838 $res.= "endobj"; 839 } 840 841 return $res; 842 } 843 } 844 845 /** 846 * a font descriptor, needed for including additional fonts 847 */ 848 protected function o_fontDescriptor($id, $action, $options = '') { 849 if ($action !== 'new') { 850 $o = & $this->objects[$id]; 851 } 852 853 switch ($action) { 854 case 'new': 855 $this->objects[$id] = array('t' => 'fontDescriptor', 'info' => $options); 856 break; 857 858 case 'out': 859 $res = "\n$id 0 obj\n<< /Type /FontDescriptor\n"; 860 foreach ($o['info'] as $label => $value) { 861 switch ($label) { 862 case 'Ascent': 863 case 'CapHeight': 864 case 'Descent': 865 case 'Flags': 866 case 'ItalicAngle': 867 case 'StemV': 868 case 'AvgWidth': 869 case 'Leading': 870 case 'MaxWidth': 871 case 'MissingWidth': 872 case 'StemH': 873 case 'XHeight': 874 case 'CharSet': 875 if (mb_strlen($value, '8bit')) { 876 $res.= "/$label $value\n"; 877 } 878 879 break; 880 case 'FontFile': 881 case 'FontFile2': 882 case 'FontFile3': 883 $res.= "/$label $value 0 R\n"; 884 break; 885 886 case 'FontBBox': 887 $res.= "/$label [$value[0] $value[1] $value[2] $value[3]]\n"; 888 break; 889 890 case 'FontName': 891 $res.= "/$label /$value\n"; 892 break; 893 } 894 } 895 896 $res.= ">>\nendobj"; 897 898 return $res; 899 } 900 } 901 902 /** 903 * the font encoding 904 */ 905 protected function o_fontEncoding($id, $action, $options = '') { 906 if ($action !== 'new') { 907 $o = & $this->objects[$id]; 908 } 909 910 switch ($action) { 911 case 'new': 912 // the options array should contain 'differences' and maybe 'encoding' 913 $this->objects[$id] = array('t' => 'fontEncoding', 'info' => $options); 914 break; 915 916 case 'out': 917 $res = "\n$id 0 obj\n<< /Type /Encoding\n"; 918 if (!isset($o['info']['encoding'])) { 919 $o['info']['encoding'] = 'WinAnsiEncoding'; 920 } 921 922 if ($o['info']['encoding'] !== 'none') { 923 $res.= "/BaseEncoding /".$o['info']['encoding']."\n"; 924 } 925 926 $res.= "/Differences \n["; 927 928 $onum = -100; 929 930 foreach ($o['info']['differences'] as $num => $label) { 931 if ($num != $onum+1) { 932 // we cannot make use of consecutive numbering 933 $res.= "\n$num /$label"; 934 } else { 935 $res.= " /$label"; 936 } 937 938 $onum = $num; 939 } 940 941 $res.= "\n]\n>>\nendobj"; 942 return $res; 943 } 944 } 945 946 /** 947 * a descendent cid font, needed for unicode fonts 948 */ 949 protected function o_fontDescendentCID($id, $action, $options = '') { 950 if ($action !== 'new') { 951 $o = & $this->objects[$id]; 952 } 953 954 switch ($action) { 955 case 'new': 956 $this->objects[$id] = array('t' => 'fontDescendentCID', 'info' => $options); 957 958 // we need a CID system info section 959 $cidSystemInfoId = ++$this->numObj; 960 $this->o_contents($cidSystemInfoId, 'new', 'raw'); 961 $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId; 962 $res = "<</Registry (Adobe)\n"; // A string identifying an issuer of character collections 963 $res.= "/Ordering (UCS)\n"; // A string that uniquely names a character collection issued by a specific registry 964 $res.= "/Supplement 0\n"; // The supplement number of the character collection. 965 $res.= ">>"; 966 $this->objects[$cidSystemInfoId]['c'] = $res; 967 968 // and a CID to GID map 969 $cidToGidMapId = ++$this->numObj; 970 $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options); 971 $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId; 972 break; 973 974 case 'add': 975 foreach ($options as $k => $v) { 976 switch ($k) { 977 case 'BaseFont': 978 $o['info']['name'] = $v; 979 break; 980 981 case 'FirstChar': 982 case 'LastChar': 983 case 'MissingWidth': 984 case 'FontDescriptor': 985 case 'SubType': 986 $this->addMessage("o_fontDescendentCID $k : $v"); 987 $o['info'][$k] = $v; 988 break; 989 } 990 } 991 992 // pass values down to cid to gid map 993 $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options); 994 break; 995 996 case 'out': 997 $res = "\n$id 0 obj\n"; 998 $res.= "<</Type /Font\n"; 999 $res.= "/Subtype /CIDFontType2\n"; 1000 $res.= "/BaseFont /".$o['info']['name']."\n"; 1001 $res.= "/CIDSystemInfo ".$o['info']['cidSystemInfo']." 0 R\n"; 1002// if (isset($o['info']['FirstChar'])) { 1003// $res.= "/FirstChar ".$o['info']['FirstChar']."\n"; 1004// } 1005 1006// if (isset($o['info']['LastChar'])) { 1007// $res.= "/LastChar ".$o['info']['LastChar']."\n"; 1008// } 1009 if (isset($o['info']['FontDescriptor'])) { 1010 $res.= "/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n"; 1011 } 1012 1013 if (isset($o['info']['MissingWidth'])) { 1014 $res.= "/DW ".$o['info']['MissingWidth']."\n"; 1015 } 1016 1017 if (isset($o['info']['fontFileName']) && isset($this->fonts[$o['info']['fontFileName']]['CIDWidths'])) { 1018 $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths']; 1019 $w = ''; 1020 foreach ($cid_widths as $cid => $width) { 1021 $w .= "$cid [$width] "; 1022 } 1023 $res.= "/W [$w]\n"; 1024 } 1025 1026 $res.= "/CIDToGIDMap ".$o['info']['cidToGidMap']." 0 R\n"; 1027 $res.= ">>\n"; 1028 $res.= "endobj"; 1029 1030 return $res; 1031 } 1032 } 1033 1034 /** 1035 * a font glyph to character map, needed for unicode fonts 1036 */ 1037 protected function o_fontGIDtoCIDMap($id, $action, $options = '') { 1038 if ($action !== 'new') { 1039 $o = & $this->objects[$id]; 1040 } 1041 1042 switch ($action) { 1043 case 'new': 1044 $this->objects[$id] = array('t' => 'fontGIDtoCIDMap', 'info' => $options); 1045 break; 1046 1047 case 'out': 1048 $res = "\n$id 0 obj\n"; 1049 $fontFileName = $o['info']['fontFileName']; 1050 $tmp = $this->fonts[$fontFileName]['CIDtoGID'] = base64_decode($this->fonts[$fontFileName]['CIDtoGID']); 1051 1052 $compressed = isset($this->fonts[$fontFileName]['CIDtoGID_Compressed']) && 1053 $this->fonts[$fontFileName]['CIDtoGID_Compressed']; 1054 1055 if (!$compressed && isset($o['raw'])) { 1056 $res.= $tmp; 1057 } else { 1058 $res.= "<<"; 1059 1060 if (!$compressed && $this->compressionReady && $this->options['compression']) { 1061 // then implement ZLIB based compression on this content stream 1062 $compressed = true; 1063 $tmp = gzcompress($tmp, 6); 1064 } 1065 if ($compressed) { 1066 $res.= "\n/Filter /FlateDecode"; 1067 } 1068 1069 $res.= "\n/Length ".mb_strlen($tmp, '8bit') .">>\nstream\n$tmp\nendstream"; 1070 } 1071 1072 $res.= "\nendobj"; 1073 return $res; 1074 } 1075 } 1076 1077 /** 1078 * the document procset, solves some problems with printing to old PS printers 1079 */ 1080 protected function o_procset($id, $action, $options = '') { 1081 if ($action !== 'new') { 1082 $o = & $this->objects[$id]; 1083 } 1084 1085 switch ($action) { 1086 case 'new': 1087 $this->objects[$id] = array('t' => 'procset', 'info' => array('PDF' => 1, 'Text' => 1)); 1088 $this->o_pages($this->currentNode, 'procset', $id); 1089 $this->procsetObjectId = $id; 1090 break; 1091 1092 case 'add': 1093 // this is to add new items to the procset list, despite the fact that this is considered 1094 // obselete, the items are required for printing to some postscript printers 1095 switch ($options) { 1096 case 'ImageB': 1097 case 'ImageC': 1098 case 'ImageI': 1099 $o['info'][$options] = 1; 1100 break; 1101 } 1102 break; 1103 1104 case 'out': 1105 $res = "\n$id 0 obj\n["; 1106 foreach ($o['info'] as $label => $val) { 1107 $res.= "/$label "; 1108 } 1109 $res.= "]\nendobj"; 1110 return $res; 1111 } 1112 } 1113 1114 /** 1115 * define the document information 1116 */ 1117 protected function o_info($id, $action, $options = '') { 1118 if ($action !== 'new') { 1119 $o = & $this->objects[$id]; 1120 } 1121 1122 switch ($action) { 1123 case 'new': 1124 $this->infoObject = $id; 1125 $date = 'D:'.@date('Ymd'); 1126 $this->objects[$id] = array('t' => 'info', 'info' => array('Creator' => 'R and OS php pdf writer, http://www.ros.co.nz', 'CreationDate' => $date)); 1127 break; 1128 case 'Title': 1129 case 'Author': 1130 case 'Subject': 1131 case 'Keywords': 1132 case 'Creator': 1133 case 'Producer': 1134 case 'CreationDate': 1135 case 'ModDate': 1136 case 'Trapped': 1137 $o['info'][$action] = $options; 1138 break; 1139 1140 case 'out': 1141 if ($this->encrypted) { 1142 $this->encryptInit($id); 1143 } 1144 1145 $res = "\n$id 0 obj\n<<\n"; 1146 foreach ($o['info'] as $k => $v) { 1147 $res.= "/$k ("; 1148 1149 if ($this->encrypted) { 1150 $v = $this->ARC4($v); 1151 } 1152 1153 // dates must be outputted as-is, without Unicode transformations 1154 elseif (!in_array($k, array('CreationDate', 'ModDate'))){ 1155 $v = $this->filterText($v); 1156 } 1157 1158 $res.= $v; 1159 $res.= ")\n"; 1160 } 1161 1162 $res.= ">>\nendobj"; 1163 return $res; 1164 } 1165 } 1166 1167 /** 1168 * an action object, used to link to URLS initially 1169 */ 1170 protected function o_action($id, $action, $options = '') { 1171 if ($action !== 'new') { 1172 $o = & $this->objects[$id]; 1173 } 1174 1175 switch ($action) { 1176 case 'new': 1177 if (is_array($options)) { 1178 $this->objects[$id] = array('t' => 'action', 'info' => $options, 'type' => $options['type']); 1179 } else { 1180 // then assume a URI action 1181 $this->objects[$id] = array('t' => 'action', 'info' => $options, 'type' => 'URI'); 1182 } 1183 break; 1184 1185 case 'out': 1186 if ($this->encrypted) { 1187 $this->encryptInit($id); 1188 } 1189 1190 $res = "\n$id 0 obj\n<< /Type /Action"; 1191 switch ($o['type']) { 1192 case 'ilink': 1193 if (!isset($this->destinations[(string)$o['info']['label']])) break; 1194 1195 // there will be an 'label' setting, this is the name of the destination 1196 $res.= "\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R"; 1197 break; 1198 1199 case 'URI': 1200 $res.= "\n/S /URI\n/URI ("; 1201 if ($this->encrypted) { 1202 $res.= $this->filterText($this->ARC4($o['info']), true, false); 1203 } else { 1204 $res.= $this->filterText($o['info'], true, false); 1205 } 1206 1207 $res.= ")"; 1208 break; 1209 } 1210 1211 $res.= "\n>>\nendobj"; 1212 return $res; 1213 } 1214 } 1215 1216 /** 1217 * an annotation object, this will add an annotation to the current page. 1218 * initially will support just link annotations 1219 */ 1220 protected function o_annotation($id, $action, $options = '') { 1221 if ($action !== 'new') { 1222 $o = & $this->objects[$id]; 1223 } 1224 1225 switch ($action) { 1226 case 'new': 1227 // add the annotation to the current page 1228 $pageId = $this->currentPage; 1229 $this->o_page($pageId, 'annot', $id); 1230 1231 // and add the action object which is going to be required 1232 switch ($options['type']) { 1233 case 'link': 1234 $this->objects[$id] = array('t' => 'annotation', 'info' => $options); 1235 $this->numObj++; 1236 $this->o_action($this->numObj, 'new', $options['url']); 1237 $this->objects[$id]['info']['actionId'] = $this->numObj; 1238 break; 1239 1240 case 'ilink': 1241 // this is to a named internal link 1242 $label = $options['label']; 1243 $this->objects[$id] = array('t' => 'annotation', 'info' => $options); 1244 $this->numObj++; 1245 $this->o_action($this->numObj, 'new', array('type' => 'ilink', 'label' => $label)); 1246 $this->objects[$id]['info']['actionId'] = $this->numObj; 1247 break; 1248 } 1249 break; 1250 1251 case 'out': 1252 $res = "\n$id 0 obj\n<< /Type /Annot"; 1253 switch ($o['info']['type']) { 1254 case 'link': 1255 case 'ilink': 1256 $res.= "\n/Subtype /Link"; 1257 break; 1258 } 1259 $res.= "\n/A ".$o['info']['actionId']." 0 R"; 1260 $res.= "\n/Border [0 0 0]"; 1261 $res.= "\n/H /I"; 1262 $res.= "\n/Rect [ "; 1263 1264 foreach ($o['info']['rect'] as $v) { 1265 $res.= sprintf("%.4F ", $v); 1266 } 1267 1268 $res.= "]"; 1269 $res.= "\n>>\nendobj"; 1270 return $res; 1271 } 1272 } 1273 1274 /** 1275 * a page object, it also creates a contents object to hold its contents 1276 */ 1277 protected function o_page($id, $action, $options = '') { 1278 if ($action !== 'new') { 1279 $o = & $this->objects[$id]; 1280 } 1281 1282 switch ($action) { 1283 case 'new': 1284 $this->numPages++; 1285 $this->objects[$id] = array('t' => 'page', 'info' => array('parent' => $this->currentNode, 'pageNum' => $this->numPages)); 1286 1287 if (is_array($options)) { 1288 // then this must be a page insertion, array should contain 'rid','pos'=[before|after] 1289 $options['id'] = $id; 1290 $this->o_pages($this->currentNode, 'page', $options); 1291 } else { 1292 $this->o_pages($this->currentNode, 'page', $id); 1293 } 1294 1295 $this->currentPage = $id; 1296 //make a contents object to go with this page 1297 $this->numObj++; 1298 $this->o_contents($this->numObj, 'new', $id); 1299 $this->currentContents = $this->numObj; 1300 $this->objects[$id]['info']['contents'] = array(); 1301 $this->objects[$id]['info']['contents'][] = $this->numObj; 1302 1303 $match = ($this->numPages%2 ? 'odd' : 'even'); 1304 foreach ($this->addLooseObjects as $oId => $target) { 1305 if ($target === 'all' || $match === $target) { 1306 $this->objects[$id]['info']['contents'][] = $oId; 1307 } 1308 } 1309 break; 1310 1311 case 'content': 1312 $o['info']['contents'][] = $options; 1313 break; 1314 1315 case 'annot': 1316 // add an annotation to this page 1317 if (!isset($o['info']['annot'])) { 1318 $o['info']['annot'] = array(); 1319 } 1320 1321 // $options should contain the id of the annotation dictionary 1322 $o['info']['annot'][] = $options; 1323 break; 1324 1325 case 'out': 1326 $res = "\n$id 0 obj\n<< /Type /Page"; 1327 $res.= "\n/Parent ".$o['info']['parent']." 0 R"; 1328 1329 if (isset($o['info']['annot'])) { 1330 $res.= "\n/Annots ["; 1331 foreach ($o['info']['annot'] as $aId) { 1332 $res.= " $aId 0 R"; 1333 } 1334 $res.= " ]"; 1335 } 1336 1337 $count = count($o['info']['contents']); 1338 if ($count == 1) { 1339 $res.= "\n/Contents ".$o['info']['contents'][0]." 0 R"; 1340 } else if ($count > 1) { 1341 $res.= "\n/Contents [\n"; 1342 1343 // reverse the page contents so added objects are below normal content 1344 //foreach (array_reverse($o['info']['contents']) as $cId) { 1345 // Back to normal now that I've got transparency working --Benj 1346 foreach ($o['info']['contents'] as $cId) { 1347 $res.= "$cId 0 R\n"; 1348 } 1349 $res.= "]"; 1350 } 1351 1352 $res.= "\n>>\nendobj"; 1353 return $res; 1354 } 1355 } 1356 1357 /** 1358 * the contents objects hold all of the content which appears on pages 1359 */ 1360 protected function o_contents($id, $action, $options = '') { 1361 if ($action !== 'new') { 1362 $o = & $this->objects[$id]; 1363 } 1364 1365 switch ($action) { 1366 case 'new': 1367 $this->objects[$id] = array('t' => 'contents', 'c' => '', 'info' => array()); 1368 if (mb_strlen($options, '8bit') && intval($options)) { 1369 // then this contents is the primary for a page 1370 $this->objects[$id]['onPage'] = $options; 1371 } else if ($options === 'raw') { 1372 // then this page contains some other type of system object 1373 $this->objects[$id]['raw'] = 1; 1374 } 1375 break; 1376 1377 case 'add': 1378 // add more options to the decleration 1379 foreach ($options as $k => $v) { 1380 $o['info'][$k] = $v; 1381 } 1382 1383 case 'out': 1384 $tmp = $o['c']; 1385 $res = "\n$id 0 obj\n"; 1386 1387 if (isset($this->objects[$id]['raw'])) { 1388 $res.= $tmp; 1389 } else { 1390 $res.= "<<"; 1391 if ($this->compressionReady && $this->options['compression']) { 1392 // then implement ZLIB based compression on this content stream 1393 $res.= " /Filter /FlateDecode"; 1394 $tmp = gzcompress($tmp, 6); 1395 } 1396 1397 if ($this->encrypted) { 1398 $this->encryptInit($id); 1399 $tmp = $this->ARC4($tmp); 1400 } 1401 1402 foreach ($o['info'] as $k => $v) { 1403 $res.= "\n/$k $v"; 1404 } 1405 1406 $res.= "\n/Length ".mb_strlen($tmp, '8bit') ." >>\nstream\n$tmp\nendstream"; 1407 } 1408 1409 $res.= "\nendobj"; 1410 return $res; 1411 } 1412 } 1413 1414 protected function o_embedjs($id, $action) { 1415 if ($action !== 'new') { 1416 $o = & $this->objects[$id]; 1417 } 1418 1419 switch ($action) { 1420 case 'new': 1421 $this->objects[$id] = array('t' => 'embedjs', 'info' => array( 1422 'Names' => '[(EmbeddedJS) '.($id+1).' 0 R]' 1423 )); 1424 break; 1425 1426 case 'out': 1427 $res = "\n$id 0 obj\n<< "; 1428 foreach ($o['info'] as $k => $v) { 1429 $res.= "\n/$k $v"; 1430 } 1431 $res.= "\n>>\nendobj"; 1432 return $res; 1433 } 1434 } 1435 1436 protected function o_javascript($id, $action, $code = '') { 1437 if ($action !== 'new') { 1438 $o = & $this->objects[$id]; 1439 } 1440 1441 switch ($action) { 1442 case 'new': 1443 $this->objects[$id] = array('t' => 'javascript', 'info' => array( 1444 'S' => '/JavaScript', 1445 'JS' => '('.$this->filterText($code).')', 1446 )); 1447 break; 1448 1449 case 'out': 1450 $res = "\n$id 0 obj\n<< "; 1451 foreach ($o['info'] as $k => $v) { 1452 $res.= "\n/$k $v"; 1453 } 1454 $res.= "\n>>\nendobj"; 1455 return $res; 1456 } 1457 } 1458 1459 /** 1460 * an image object, will be an XObject in the document, includes description and data 1461 */ 1462 protected function o_image($id, $action, $options = '') { 1463 if ($action !== 'new') { 1464 $o = & $this->objects[$id]; 1465 } 1466 1467 switch ($action) { 1468 case 'new': 1469 // make the new object 1470 $this->objects[$id] = array('t' => 'image', 'data' => &$options['data'], 'info' => array()); 1471 1472 $info =& $this->objects[$id]['info']; 1473 1474 $info['Type'] = '/XObject'; 1475 $info['Subtype'] = '/Image'; 1476 $info['Width'] = $options['iw']; 1477 $info['Height'] = $options['ih']; 1478 1479 if (isset($options['masked']) && $options['masked']) { 1480 $info['SMask'] = ($this->numObj-1).' 0 R'; 1481 } 1482 1483 if (!isset($options['type']) || $options['type'] === 'jpg') { 1484 if (!isset($options['channels'])) { 1485 $options['channels'] = 3; 1486 } 1487 1488 switch ($options['channels']) { 1489 case 1: $info['ColorSpace'] = '/DeviceGray'; break; 1490 case 4: $info['ColorSpace'] = '/DeviceCMYK'; break; 1491 default: $info['ColorSpace'] = '/DeviceRGB'; break; 1492 } 1493 1494 if ($info['ColorSpace'] === '/DeviceCMYK') { 1495 $info['Decode'] = '[1 0 1 0 1 0 1 0]'; 1496 } 1497 1498 $info['Filter'] = '/DCTDecode'; 1499 $info['BitsPerComponent'] = 8; 1500 } 1501 1502 else if ($options['type'] === 'png') { 1503 $info['Filter'] = '/FlateDecode'; 1504 $info['DecodeParms'] = '<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>'; 1505 1506 if ($options['isMask']) { 1507 $info['ColorSpace'] = '/DeviceGray'; 1508 } 1509 else { 1510 if (mb_strlen($options['pdata'], '8bit')) { 1511 $tmp = ' [ /Indexed /DeviceRGB '.(mb_strlen($options['pdata'], '8bit') /3-1) .' '; 1512 $this->numObj++; 1513 $this->o_contents($this->numObj, 'new'); 1514 $this->objects[$this->numObj]['c'] = $options['pdata']; 1515 $tmp.= $this->numObj.' 0 R'; 1516 $tmp.= ' ]'; 1517 $info['ColorSpace'] = $tmp; 1518 1519 if (isset($options['transparency'])) { 1520 $transparency = $options['transparency']; 1521 switch ($transparency['type']) { 1522 case 'indexed': 1523 $tmp = ' [ '.$transparency['data'].' '.$transparency['data'].'] '; 1524 $info['Mask'] = $tmp; 1525 break; 1526 1527 case 'color-key': 1528 $tmp = ' [ '. 1529 $transparency['r'] . ' ' . $transparency['r'] . 1530 $transparency['g'] . ' ' . $transparency['g'] . 1531 $transparency['b'] . ' ' . $transparency['b'] . 1532 ' ] '; 1533 $info['Mask'] = $tmp; 1534 break; 1535 } 1536 } 1537 } else { 1538 if (isset($options['transparency'])) { 1539 $transparency = $options['transparency']; 1540 1541 switch ($transparency['type']) { 1542 case 'indexed': 1543 $tmp = ' [ '.$transparency['data'].' '.$transparency['data'].'] '; 1544 $info['Mask'] = $tmp; 1545 break; 1546 1547 case 'color-key': 1548 $tmp = ' [ '. 1549 $transparency['r'] . ' ' . $transparency['r'] . ' ' . 1550 $transparency['g'] . ' ' . $transparency['g'] . ' ' . 1551 $transparency['b'] . ' ' . $transparency['b'] . 1552 ' ] '; 1553 $info['Mask'] = $tmp; 1554 break; 1555 } 1556 } 1557 $info['ColorSpace'] = '/'.$options['color']; 1558 } 1559 } 1560 1561 $info['BitsPerComponent'] = $options['bitsPerComponent']; 1562 } 1563 1564 // assign it a place in the named resource dictionary as an external object, according to 1565 // the label passed in with it. 1566 $this->o_pages($this->currentNode, 'xObject', array('label' => $options['label'], 'objNum' => $id)); 1567 1568 // also make sure that we have the right procset object for it. 1569 $this->o_procset($this->procsetObjectId, 'add', 'ImageC'); 1570 break; 1571 1572 case 'out': 1573 $tmp = &$o['data']; 1574 $res = "\n$id 0 obj\n<<"; 1575 1576 foreach ($o['info'] as $k => $v) { 1577 $res.= "\n/$k $v"; 1578 } 1579 1580 if ($this->encrypted) { 1581 $this->encryptInit($id); 1582 $tmp = $this->ARC4($tmp); 1583 } 1584 1585 $res.= "\n/Length ".mb_strlen($tmp, '8bit') .">>\nstream\n$tmp\nendstream\nendobj"; 1586 1587 return $res; 1588 } 1589 } 1590 1591 /** 1592 * graphics state object 1593 */ 1594 protected function o_extGState($id, $action, $options = "") { 1595 static $valid_params = array("LW", "LC", "LC", "LJ", "ML", 1596 "D", "RI", "OP", "op", "OPM", 1597 "Font", "BG", "BG2", "UCR", 1598 "TR", "TR2", "HT", "FL", 1599 "SM", "SA", "BM", "SMask", 1600 "CA", "ca", "AIS", "TK"); 1601 1602 if ($action !== "new") { 1603 $o = & $this->objects[$id]; 1604 } 1605 1606 switch ($action) { 1607 case "new": 1608 $this->objects[$id] = array('t' => 'extGState', 'info' => $options); 1609 1610 // Tell the pages about the new resource 1611 $this->numStates++; 1612 $this->o_pages($this->currentNode, 'extGState', array("objNum" => $id, "stateNum" => $this->numStates)); 1613 break; 1614 1615 case "out": 1616 $res = "\n$id 0 obj\n<< /Type /ExtGState\n"; 1617 1618 foreach ($o["info"] as $k => $v) { 1619 if ( !in_array($k, $valid_params)) 1620 continue; 1621 $res.= "/$k $v\n"; 1622 } 1623 1624 $res.= ">>\nendobj"; 1625 return $res; 1626 } 1627 } 1628 1629 /** 1630 * encryption object. 1631 */ 1632 protected function o_encryption($id, $action, $options = '') { 1633 if ($action !== 'new') { 1634 $o = & $this->objects[$id]; 1635 } 1636 1637 switch ($action) { 1638 case 'new': 1639 // make the new object 1640 $this->objects[$id] = array('t' => 'encryption', 'info' => $options); 1641 $this->arc4_objnum = $id; 1642 1643 // figure out the additional paramaters required 1644 $pad = chr(0x28) .chr(0xBF) .chr(0x4E) .chr(0x5E) .chr(0x4E) .chr(0x75) .chr(0x8A) .chr(0x41) 1645 .chr(0x64) .chr(0x00) .chr(0x4E) .chr(0x56) .chr(0xFF) .chr(0xFA) .chr(0x01) .chr(0x08) 1646 .chr(0x2E) .chr(0x2E) .chr(0x00) .chr(0xB6) .chr(0xD0) .chr(0x68) .chr(0x3E) .chr(0x80) 1647 .chr(0x2F) .chr(0x0C) .chr(0xA9) .chr(0xFE) .chr(0x64) .chr(0x53) .chr(0x69) .chr(0x7A); 1648 1649 $len = mb_strlen($options['owner'], '8bit'); 1650 1651 if ($len > 32) { 1652 $owner = substr($options['owner'], 0, 32); 1653 } else if ($len < 32) { 1654 $owner = $options['owner'].substr($pad, 0, 32-$len); 1655 } else { 1656 $owner = $options['owner']; 1657 } 1658 1659 $len = mb_strlen($options['user'], '8bit'); 1660 if ($len > 32) { 1661 $user = substr($options['user'], 0, 32); 1662 } else if ($len < 32) { 1663 $user = $options['user'].substr($pad, 0, 32-$len); 1664 } else { 1665 $user = $options['user']; 1666 } 1667 1668 $tmp = $this->md5_16($owner); 1669 $okey = substr($tmp, 0, 5); 1670 $this->ARC4_init($okey); 1671 $ovalue = $this->ARC4($user); 1672 $this->objects[$id]['info']['O'] = $ovalue; 1673 1674 // now make the u value, phew. 1675 $tmp = $this->md5_16($user.$ovalue.chr($options['p']) .chr(255) .chr(255) .chr(255) .$this->fileIdentifier); 1676 1677 $ukey = substr($tmp, 0, 5); 1678 $this->ARC4_init($ukey); 1679 $this->encryptionKey = $ukey; 1680 $this->encrypted = true; 1681 $uvalue = $this->ARC4($pad); 1682 $this->objects[$id]['info']['U'] = $uvalue; 1683 $this->encryptionKey = $ukey; 1684 // initialize the arc4 array 1685 break; 1686 1687 case 'out': 1688 $res = "\n$id 0 obj\n<<"; 1689 $res.= "\n/Filter /Standard"; 1690 $res.= "\n/V 1"; 1691 $res.= "\n/R 2"; 1692 $res.= "\n/O (".$this->filterText($o['info']['O'], true, false) .')'; 1693 $res.= "\n/U (".$this->filterText($o['info']['U'], true, false) .')'; 1694 // and the p-value needs to be converted to account for the twos-complement approach 1695 $o['info']['p'] = (($o['info']['p']^255) +1) *-1; 1696 $res.= "\n/P ".($o['info']['p']); 1697 $res.= "\n>>\nendobj"; 1698 return $res; 1699 } 1700 } 1701 1702 /** 1703 * ARC4 functions 1704 * A series of function to implement ARC4 encoding in PHP 1705 */ 1706 1707 /** 1708 * calculate the 16 byte version of the 128 bit md5 digest of the string 1709 */ 1710 function md5_16($string) { 1711 $tmp = md5($string); 1712 $out = ''; 1713 for ($i = 0; $i <= 30; $i = $i+2) { 1714 $out.= chr(hexdec(substr($tmp, $i, 2))); 1715 } 1716 return $out; 1717 } 1718 1719 /** 1720 * initialize the encryption for processing a particular object 1721 */ 1722 function encryptInit($id) { 1723 $tmp = $this->encryptionKey; 1724 $hex = dechex($id); 1725 if (mb_strlen($hex, '8bit') < 6) { 1726 $hex = substr('000000', 0, 6-mb_strlen($hex, '8bit')) .$hex; 1727 } 1728 $tmp.= chr(hexdec(substr($hex, 4, 2))) .chr(hexdec(substr($hex, 2, 2))) .chr(hexdec(substr($hex, 0, 2))) .chr(0) .chr(0); 1729 $key = $this->md5_16($tmp); 1730 $this->ARC4_init(substr($key, 0, 10)); 1731 } 1732 1733 /** 1734 * initialize the ARC4 encryption 1735 */ 1736 function ARC4_init($key = '') { 1737 $this->arc4 = ''; 1738 1739 // setup the control array 1740 if (mb_strlen($key, '8bit') == 0) { 1741 return; 1742 } 1743 1744 $k = ''; 1745 while (mb_strlen($k, '8bit') < 256) { 1746 $k.= $key; 1747 } 1748 1749 $k = substr($k, 0, 256); 1750 for ($i = 0; $i < 256; $i++) { 1751 $this->arc4.= chr($i); 1752 } 1753 1754 $j = 0; 1755 1756 for ($i = 0; $i < 256; $i++) { 1757 $t = $this->arc4[$i]; 1758 $j = ($j + ord($t) + ord($k[$i])) %256; 1759 $this->arc4[$i] = $this->arc4[$j]; 1760 $this->arc4[$j] = $t; 1761 } 1762 } 1763 1764 /** 1765 * ARC4 encrypt a text string 1766 */ 1767 function ARC4($text) { 1768 $len = mb_strlen($text, '8bit'); 1769 $a = 0; 1770 $b = 0; 1771 $c = $this->arc4; 1772 $out = ''; 1773 for ($i = 0; $i < $len; $i++) { 1774 $a = ($a+1) %256; 1775 $t = $c[$a]; 1776 $b = ($b+ord($t)) %256; 1777 $c[$a] = $c[$b]; 1778 $c[$b] = $t; 1779 $k = ord($c[(ord($c[$a]) + ord($c[$b])) %256]); 1780 $out.= chr(ord($text[$i]) ^ $k); 1781 } 1782 return $out; 1783 } 1784 1785 /** 1786 * functions which can be called to adjust or add to the document 1787 */ 1788 1789 /** 1790 * add a link in the document to an external URL 1791 */ 1792 function addLink($url, $x0, $y0, $x1, $y1) { 1793 $this->numObj++; 1794 $info = array('type' => 'link', 'url' => $url, 'rect' => array($x0, $y0, $x1, $y1)); 1795 $this->o_annotation($this->numObj, 'new', $info); 1796 } 1797 1798 /** 1799 * add a link in the document to an internal destination (ie. within the document) 1800 */ 1801 function addInternalLink($label, $x0, $y0, $x1, $y1) { 1802 $this->numObj++; 1803 $info = array('type' => 'ilink', 'label' => $label, 'rect' => array($x0, $y0, $x1, $y1)); 1804 $this->o_annotation($this->numObj, 'new', $info); 1805 } 1806 1807 /** 1808 * set the encryption of the document 1809 * can be used to turn it on and/or set the passwords which it will have. 1810 * also the functions that the user will have are set here, such as print, modify, add 1811 */ 1812 function setEncryption($userPass = '', $ownerPass = '', $pc = array()) { 1813 $p = bindec("11000000"); 1814 1815 $options = array('print' => 4, 'modify' => 8, 'copy' => 16, 'add' => 32); 1816 1817 foreach ($pc as $k => $v) { 1818 if ($v && isset($options[$k])) { 1819 $p+= $options[$k]; 1820 } else if (isset($options[$v])) { 1821 $p+= $options[$v]; 1822 } 1823 } 1824 1825 // implement encryption on the document 1826 if ($this->arc4_objnum == 0) { 1827 // then the block does not exist already, add it. 1828 $this->numObj++; 1829 if (mb_strlen($ownerPass) == 0) { 1830 $ownerPass = $userPass; 1831 } 1832 1833 $this->o_encryption($this->numObj, 'new', array('user' => $userPass, 'owner' => $ownerPass, 'p' => $p)); 1834 } 1835 } 1836 1837 /** 1838 * should be used for internal checks, not implemented as yet 1839 */ 1840 function checkAllHere() { 1841 } 1842 1843 /** 1844 * return the pdf stream as a string returned from the function 1845 */ 1846 function output($debug = false) { 1847 if ($debug) { 1848 // turn compression off 1849 $this->options['compression'] = false; 1850 } 1851 1852 if ($this->javascript) { 1853 $this->numObj++; 1854 1855 $js_id = $this->numObj; 1856 $this->o_embedjs($js_id, 'new'); 1857 $this->o_javascript(++$this->numObj, 'new', $this->javascript); 1858 1859 $id = $this->catalogId; 1860 1861 $this->o_catalog($id, 'javascript', $js_id); 1862 } 1863 1864 if ($this->arc4_objnum) { 1865 $this->ARC4_init($this->encryptionKey); 1866 } 1867 1868 $this->checkAllHere(); 1869 1870 $xref = array(); 1871 $content = '%PDF-1.3'; 1872 $pos = mb_strlen($content, '8bit'); 1873 1874 foreach ($this->objects as $k => $v) { 1875 $tmp = 'o_'.$v['t']; 1876 $cont = $this->$tmp($k, 'out'); 1877 $content.= $cont; 1878 $xref[] = $pos; 1879 $pos+= mb_strlen($cont, '8bit'); 1880 } 1881 1882 $content.= "\nxref\n0 ".(count($xref) +1) ."\n0000000000 65535 f \n"; 1883 1884 foreach ($xref as $p) { 1885 $content.= str_pad($p, 10, "0", STR_PAD_LEFT) . " 00000 n \n"; 1886 } 1887 1888 $content.= "trailer\n<<\n/Size ".(count($xref) +1) ."\n/Root 1 0 R\n/Info $this->infoObject 0 R\n"; 1889 1890 // if encryption has been applied to this document then add the marker for this dictionary 1891 if ($this->arc4_objnum > 0) { 1892 $content.= "/Encrypt $this->arc4_objnum 0 R\n"; 1893 } 1894 1895 if (mb_strlen($this->fileIdentifier, '8bit')) { 1896 $content.= "/ID[<$this->fileIdentifier><$this->fileIdentifier>]\n"; 1897 } 1898 1899 $content.= ">>\nstartxref\n$pos\n%%EOF\n"; 1900 1901 return $content; 1902 } 1903 1904 /** 1905 * intialize a new document 1906 * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum 1907 * this function is called automatically by the constructor function 1908 */ 1909 private function newDocument($pageSize = array(0, 0, 612, 792)) { 1910 $this->numObj = 0; 1911 $this->objects = array(); 1912 1913 $this->numObj++; 1914 $this->o_catalog($this->numObj, 'new'); 1915 1916 $this->numObj++; 1917 $this->o_outlines($this->numObj, 'new'); 1918 1919 $this->numObj++; 1920 $this->o_pages($this->numObj, 'new'); 1921 1922 $this->o_pages($this->numObj, 'mediaBox', $pageSize); 1923 $this->currentNode = 3; 1924 1925 $this->numObj++; 1926 $this->o_procset($this->numObj, 'new'); 1927 1928 $this->numObj++; 1929 $this->o_info($this->numObj, 'new'); 1930 1931 $this->numObj++; 1932 $this->o_page($this->numObj, 'new'); 1933 1934 // need to store the first page id as there is no way to get it to the user during 1935 // startup 1936 $this->firstPageId = $this->currentContents; 1937 } 1938 1939 /** 1940 * open the font file and return a php structure containing it. 1941 * first check if this one has been done before and saved in a form more suited to php 1942 * note that if a php serialized version does not exist it will try and make one, but will 1943 * require write access to the directory to do it... it is MUCH faster to have these serialized 1944 * files. 1945 */ 1946 private function openFont($font) { 1947 // assume that $font contains the path and file but not the extension 1948 $pos = strrpos($font, '/'); 1949 1950 if ($pos === false) { 1951 $dir = './'; 1952 $name = $font; 1953 } else { 1954 $dir = substr($font, 0, $pos+1); 1955 $name = substr($font, $pos+1); 1956 } 1957 1958 $fontcache = $this->fontcache; 1959 if ($fontcache == '') { 1960 $fontcache = $dir; 1961 } 1962 1963 //$name filename without folder and extension of font metrics 1964 //$dir folder of font metrics 1965 //$fontcache folder of runtime created php serialized version of font metrics. 1966 // If this is not given, the same folder as the font metrics will be used. 1967 // Storing and reusing serialized versions improves speed much 1968 1969 $this->addMessage("openFont: $font - $name"); 1970 1971 if ( !$this->isUnicode || in_array(mb_strtolower(basename($name)), self::$coreFonts) ) { 1972 $metrics_name = "$name.afm"; 1973 } 1974 else { 1975 $metrics_name = "$name.ufm"; 1976 } 1977 1978 $cache_name = "$metrics_name.php"; 1979 $this->addMessage("metrics: $metrics_name, cache: $cache_name"); 1980 1981 if (file_exists($fontcache . $cache_name)) { 1982 $this->addMessage("openFont: php file exists $fontcache$cache_name"); 1983 $this->fonts[$font] = require($fontcache . $cache_name); 1984 1985 if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_'] != $this->fontcacheVersion) { 1986 // if the font file is old, then clear it out and prepare for re-creation 1987 $this->addMessage('openFont: clear out, make way for new version.'); 1988 $this->fonts[$font] = null; 1989 unset($this->fonts[$font]); 1990 } 1991 } 1992 else { 1993 $old_cache_name = "php_$metrics_name"; 1994 if (file_exists($fontcache . $old_cache_name)) { 1995 $this->addMessage("openFont: php file doesn't exist $fontcache$cache_name, creating it from the old format"); 1996 $old_cache = file_get_contents($fontcache . $old_cache_name); 1997 file_put_contents($fontcache . $cache_name, '<?php return ' . $old_cache . ';'); 1998 return $this->openFont($font); 1999 } 2000 } 2001 2002 if (!isset($this->fonts[$font]) && file_exists($dir . $metrics_name)) { 2003 // then rebuild the php_<font>.afm file from the <font>.afm file 2004 $this->addMessage("openFont: build php file from $dir$metrics_name"); 2005 $data = array(); 2006 2007 // 20 => 'space' 2008 $data['codeToName'] = array(); 2009 2010 // Since we're not going to enable Unicode for the core fonts we need to use a font-based 2011 // setting for Unicode support rather than a global setting. 2012 $data['isUnicode'] = (strtolower(substr($metrics_name, -3)) !== 'afm'); 2013 2014 $cidtogid = ''; 2015 if ($data['isUnicode']) { 2016 $cidtogid = str_pad('', 256*256*2, "\x00"); 2017 } 2018 2019 $file = file($dir . $metrics_name); 2020 2021 foreach ($file as $rowA) { 2022 $row = trim($rowA); 2023 $pos = strpos($row, ' '); 2024 2025 if ($pos) { 2026 // then there must be some keyword 2027 $key = substr($row, 0, $pos); 2028 switch ($key) { 2029 case 'FontName': 2030 case 'FullName': 2031 case 'FamilyName': 2032 case 'PostScriptName': 2033 case 'Weight': 2034 case 'ItalicAngle': 2035 case 'IsFixedPitch': 2036 case 'CharacterSet': 2037 case 'UnderlinePosition': 2038 case 'UnderlineThickness': 2039 case 'Version': 2040 case 'EncodingScheme': 2041 case 'CapHeight': 2042 case 'XHeight': 2043 case 'Ascender': 2044 case 'Descender': 2045 case 'StdHW': 2046 case 'StdVW': 2047 case 'StartCharMetrics': 2048 case 'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font. Otherwise it's too big. 2049 $data[$key] = trim(substr($row, $pos)); 2050 break; 2051 2052 case 'FontBBox': 2053 $data[$key] = explode(' ', trim(substr($row, $pos))); 2054 break; 2055 2056 //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; 2057 case 'C': // Found in AFM files 2058 $bits = explode(';', trim($row)); 2059 $dtmp = array(); 2060 2061 foreach ($bits as $bit) { 2062 $bits2 = explode(' ', trim($bit)); 2063 if (mb_strlen($bits2[0], '8bit') == 0) continue; 2064 2065 if (count($bits2) > 2) { 2066 $dtmp[$bits2[0]] = array(); 2067 for ($i = 1; $i < count($bits2); $i++) { 2068 $dtmp[$bits2[0]][] = $bits2[$i]; 2069 } 2070 } else if (count($bits2) == 2) { 2071 $dtmp[$bits2[0]] = $bits2[1]; 2072 } 2073 } 2074 2075 $c = (int)$dtmp['C']; 2076 $n = $dtmp['N']; 2077 $width = floatval($dtmp['WX']); 2078 2079 if ($c >= 0) { 2080 if ($c != hexdec($n)) { 2081 $data['codeToName'][$c] = $n; 2082 } 2083 $data['C'][$c] = $width; 2084 } else { 2085 $data['C'][$n] = $width; 2086 } 2087 2088 if (!isset($data['MissingWidth']) && $c == -1 && $n === '.notdef') { 2089 $data['MissingWidth'] = $width; 2090 } 2091 2092 break; 2093 2094 // U 827 ; WX 0 ; N squaresubnosp ; G 675 ; 2095 case 'U': // Found in UFM files 2096 if (!$data['isUnicode']) break; 2097 2098 $bits = explode(';', trim($row)); 2099 $dtmp = array(); 2100 2101 foreach ($bits as $bit) { 2102 $bits2 = explode(' ', trim($bit)); 2103 if (mb_strlen($bits2[0], '8bit') === 0) continue; 2104 2105 if (count($bits2) > 2) { 2106 $dtmp[$bits2[0]] = array(); 2107 for ($i = 1; $i < count($bits2); $i++) { 2108 $dtmp[$bits2[0]][] = $bits2[$i]; 2109 } 2110 } else if (count($bits2) == 2) { 2111 $dtmp[$bits2[0]] = $bits2[1]; 2112 } 2113 } 2114 2115 $c = (int)$dtmp['U']; 2116 $n = $dtmp['N']; 2117 $glyph = $dtmp['G']; 2118 $width = floatval($dtmp['WX']); 2119 2120 if ($c >= 0) { 2121 // Set values in CID to GID map 2122 if ($c >= 0 && $c < 0xFFFF && $glyph) { 2123 $cidtogid[$c*2] = chr($glyph >> 8); 2124 $cidtogid[$c*2 + 1] = chr($glyph & 0xFF); 2125 } 2126 2127 if ($c != hexdec($n)) { 2128 $data['codeToName'][$c] = $n; 2129 } 2130 $data['C'][$c] = $width; 2131 } else { 2132 $data['C'][$n] = $width; 2133 } 2134 2135 if (!isset($data['MissingWidth']) && $c == -1 && $n === '.notdef') { 2136 $data['MissingWidth'] = $width; 2137 } 2138 2139 break; 2140 2141 case 'KPX': 2142 break; // don't include them as they are not used yet 2143 //KPX Adieresis yacute -40 2144 $bits = explode(' ', trim($row)); 2145 $data['KPX'][$bits[1]][$bits[2]] = $bits[3]; 2146 break; 2147 } 2148 } 2149 } 2150 2151 if ($this->compressionReady && $this->options['compression']) { 2152 // then implement ZLIB based compression on CIDtoGID string 2153 $data['CIDtoGID_Compressed'] = true; 2154 $cidtogid = gzcompress($cidtogid, 6); 2155 } 2156 $data['CIDtoGID'] = base64_encode($cidtogid); 2157 $data['_version_'] = $this->fontcacheVersion; 2158 $this->fonts[$font] = $data; 2159 2160 //Because of potential trouble with php safe mode, expect that the folder already exists. 2161 //If not existing, this will hit performance because of missing cached results. 2162 if ( is_dir(substr($fontcache, 0, -1)) && is_writable(substr($fontcache, 0, -1)) ) { 2163 file_put_contents($fontcache . $cache_name, '<?php return ' . var_export($data, true) . ';'); 2164 } 2165 $data = null; 2166 } 2167 2168 if (!isset($this->fonts[$font])) { 2169 $this->addMessage("openFont: no font file found for $font. Do you need to run load_font.php?"); 2170 } 2171 2172 //pre_r($this->messages); 2173 } 2174 2175 /** 2176 * if the font is not loaded then load it and make the required object 2177 * else just make it the current font 2178 * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding' 2179 * note that encoding='none' will need to be used for symbolic fonts 2180 * and 'differences' => an array of mappings between numbers 0->255 and character names. 2181 * 2182 */ 2183 function selectFont($fontName, $encoding = '', $set = true) { 2184 $ext = substr($fontName, -4); 2185 if ($ext === '.afm' || $ext === '.ufm') { 2186 $fontName = substr($fontName, 0, mb_strlen($fontName)-4); 2187 } 2188 2189 if (!isset($this->fonts[$fontName])) { 2190 $this->addMessage("selectFont: selecting - $fontName - $encoding, $set"); 2191 2192 // load the file 2193 $this->openFont($fontName); 2194 2195 if (isset($this->fonts[$fontName])) { 2196 $this->numObj++; 2197 $this->numFonts++; 2198 2199 $font = &$this->fonts[$fontName]; 2200 2201 //$this->numFonts = md5($fontName); 2202 $pos = strrpos($fontName, '/'); 2203 // $dir = substr($fontName,0,$pos+1); 2204 $name = substr($fontName, $pos+1); 2205 $options = array('name' => $name, 'fontFileName' => $fontName); 2206 2207 if (is_array($encoding)) { 2208 // then encoding and differences might be set 2209 if (isset($encoding['encoding'])) { 2210 $options['encoding'] = $encoding['encoding']; 2211 } 2212 2213 if (isset($encoding['differences'])) { 2214 $options['differences'] = $encoding['differences']; 2215 } 2216 } else if (mb_strlen($encoding, '8bit')) { 2217 // then perhaps only the encoding has been set 2218 $options['encoding'] = $encoding; 2219 } 2220 2221 $fontObj = $this->numObj; 2222 $this->o_font($this->numObj, 'new', $options); 2223 $font['fontNum'] = $this->numFonts; 2224 2225 // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there 2226 // should be for all non-basic fonts), then load it into an object and put the 2227 // references into the font object 2228 $basefile = $fontName; 2229 2230 $fbtype = ''; 2231 if (file_exists("$basefile.pfb")) { 2232 $fbtype = 'pfb'; 2233 } 2234 else if (file_exists("$basefile.ttf")) { 2235 $fbtype = 'ttf'; 2236 } 2237 2238 $fbfile = "$basefile.$fbtype"; 2239 2240 // $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb'; 2241 // $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf'; 2242 $this->addMessage('selectFont: checking for - '.$fbfile); 2243 2244 // OAR - I don't understand this old check 2245 // if (substr($fontName, -4) === '.afm' && strlen($fbtype)) { 2246 if ($fbtype) { 2247 $adobeFontName = isset($font['PostScriptName']) ? $font['PostScriptName'] : $font['FontName']; 2248 // $fontObj = $this->numObj; 2249 $this->addMessage("selectFont: adding font file - $fbfile - $adobeFontName"); 2250 2251 // find the array of font widths, and put that into an object. 2252 $firstChar = -1; 2253 $lastChar = 0; 2254 $widths = array(); 2255 $cid_widths = array(); 2256 2257 foreach ($font['C'] as $num => $d) { 2258 if (intval($num) > 0 || $num == '0') { 2259 if (!$font['isUnicode']) { 2260 // With Unicode, widths array isn't used 2261 if ($lastChar>0 && $num>$lastChar+1) { 2262 for ($i = $lastChar+1; $i<$num; $i++) { 2263 $widths[] = 0; 2264 } 2265 } 2266 } 2267 2268 $widths[] = $d; 2269 2270 if ($font['isUnicode']) { 2271 $cid_widths[$num] = $d; 2272 } 2273 2274 if ($firstChar == -1) { 2275 $firstChar = $num; 2276 } 2277 2278 $lastChar = $num; 2279 } 2280 } 2281 2282 // also need to adjust the widths for the differences array 2283 if (isset($options['differences'])) { 2284 foreach ($options['differences'] as $charNum => $charName) { 2285 if ($charNum > $lastChar) { 2286 if (!$font['isUnicode']) { 2287 // With Unicode, widths array isn't used 2288 for ($i = $lastChar + 1; $i <= $charNum; $i++) { 2289 $widths[] = 0; 2290 } 2291 } 2292 2293 $lastChar = $charNum; 2294 } 2295 2296 if (isset($font['C'][$charName])) { 2297 $widths[$charNum-$firstChar] = $font['C'][$charName]; 2298 if ($font['isUnicode']) { 2299 $cid_widths[$charName] = $font['C'][$charName]; 2300 } 2301 } 2302 } 2303 } 2304 2305 if ($font['isUnicode']) { 2306 $font['CIDWidths'] = $cid_widths; 2307 } 2308 2309 $this->addMessage('selectFont: FirstChar = '.$firstChar); 2310 $this->addMessage('selectFont: LastChar = '.$lastChar); 2311 2312 $widthid = -1; 2313 2314 if (!$font['isUnicode']) { 2315 // With Unicode, widths array isn't used 2316 2317 $this->numObj++; 2318 $this->o_contents($this->numObj, 'new', 'raw'); 2319 $this->objects[$this->numObj]['c'].= '['.implode(' ', $widths).']'; 2320 $widthid = $this->numObj; 2321 } 2322 2323 $missing_width = 500; 2324 $stemV = 70; 2325 2326 if (isset($font['MissingWidth'])) { 2327 $missing_width = $font['MissingWidth']; 2328 } 2329 if (isset($font['StdVW'])) { 2330 $stemV = $font['StdVW']; 2331 } else if (isset($font['Weight']) && preg_match('!(bold|black)!i', $font['Weight'])) { 2332 $stemV = 120; 2333 } 2334 2335 // load the pfb file, and put that into an object too. 2336 // note that pdf supports only binary format type 1 font files, though there is a 2337 // simple utility to convert them from pfa to pfb. 2338 if (!$this->isUnicode || $fbtype !== 'ttf' || empty($this->stringSubsets)) { 2339 $data = file_get_contents($fbfile); 2340 } 2341 else { 2342 $this->stringSubsets[$fontName][] = 32; // Force space if not in yet 2343 2344 $subset = $this->stringSubsets[$fontName]; 2345 sort($subset); 2346 2347 // Load font 2348 $font_obj = Font::load($fbfile); 2349 $font_obj->parse(); 2350 2351 // Define subset 2352 $font_obj->setSubset($subset); 2353 $font_obj->reduce(); 2354 2355 // Write new font 2356 $tmp_name = "$fbfile.tmp.".uniqid(); 2357 $font_obj->open($tmp_name, Font_Binary_Stream::modeWrite); 2358 $font_obj->encode(array("OS/2")); 2359 $font_obj->close(); 2360 2361 // Parse the new font to get cid2gid and widths 2362 $font_obj = Font::load($tmp_name); 2363 2364 // Find Unicode char map table 2365 $subtable = null; 2366 foreach ($font_obj->getData("cmap", "subtables") as $_subtable) { 2367 if ($_subtable["platformID"] == 0 || $_subtable["platformID"] == 3 && $_subtable["platformSpecificID"] == 1) { 2368 $subtable = $_subtable; 2369 break; 2370 } 2371 } 2372 2373 if ($subtable) { 2374 $glyphIndexArray = $subtable["glyphIndexArray"]; 2375 $hmtx = $font_obj->getData("hmtx"); 2376 2377 unset($glyphIndexArray[0xFFFF]); 2378 2379 $cidtogid = str_pad('', max(array_keys($glyphIndexArray))*2+1, "\x00"); 2380 $font['CIDWidths'] = array(); 2381 foreach ($glyphIndexArray as $cid => $gid) { 2382 if ($cid >= 0 && $cid < 0xFFFF && $gid) { 2383 $cidtogid[$cid*2] = chr($gid >> 8); 2384 $cidtogid[$cid*2 + 1] = chr($gid & 0xFF); 2385 } 2386 2387 $width = $font_obj->normalizeFUnit(isset($hmtx[$gid]) ? $hmtx[$gid][0] : $hmtx[0][0]); 2388 $font['CIDWidths'][$cid] = $width; 2389 } 2390 2391 $font['CIDtoGID'] = base64_encode(gzcompress($cidtogid)); 2392 $font['CIDtoGID_Compressed'] = true; 2393 2394 $data = file_get_contents($tmp_name); 2395 } 2396 else { 2397 $data = file_get_contents($fbfile); 2398 } 2399 2400 $font_obj->close(); 2401 unlink($tmp_name); 2402 } 2403 2404 // create the font descriptor 2405 $this->numObj++; 2406 $fontDescriptorId = $this->numObj; 2407 2408 $this->numObj++; 2409 $pfbid = $this->numObj; 2410 2411 // determine flags (more than a little flakey, hopefully will not matter much) 2412 $flags = 0; 2413 2414 if ($font['ItalicAngle'] != 0) { 2415 $flags+= pow(2, 6); 2416 } 2417 2418 if ($font['IsFixedPitch'] === 'true') { 2419 $flags+= 1; 2420 } 2421 2422 $flags+= pow(2, 5); // assume non-sybolic 2423 $list = array( 2424 'Ascent' => 'Ascender', 2425 'CapHeight' => 'CapHeight', 2426 'MissingWidth' => 'MissingWidth', 2427 'Descent' => 'Descender', 2428 'FontBBox' => 'FontBBox', 2429 'ItalicAngle' => 'ItalicAngle' 2430 ); 2431 $fdopt = array( 2432 'Flags' => $flags, 2433 'FontName' => $adobeFontName, 2434 'StemV' => $stemV 2435 ); 2436 2437 foreach ($list as $k => $v) { 2438 if (isset($font[$v])) { 2439 $fdopt[$k] = $font[$v]; 2440 } 2441 } 2442 2443 if ($fbtype === 'pfb') { 2444 $fdopt['FontFile'] = $pfbid; 2445 } else if ($fbtype === 'ttf') { 2446 $fdopt['FontFile2'] = $pfbid; 2447 } 2448 2449 $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt); 2450 2451 // embed the font program 2452 $this->o_contents($this->numObj, 'new'); 2453 $this->objects[$pfbid]['c'].= $data; 2454 2455 // determine the cruicial lengths within this file 2456 if ($fbtype === 'pfb') { 2457 $l1 = strpos($data, 'eexec') +6; 2458 $l2 = strpos($data, '00000000') -$l1; 2459 $l3 = mb_strlen($data, '8bit') -$l2-$l1; 2460 $this->o_contents($this->numObj, 'add', array('Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3)); 2461 } else if ($fbtype == 'ttf') { 2462 $l1 = mb_strlen($data, '8bit'); 2463 $this->o_contents($this->numObj, 'add', array('Length1' => $l1)); 2464 } 2465 2466 // tell the font object about all this new stuff 2467 $tmp = array( 2468 'BaseFont' => $adobeFontName, 2469 'MissingWidth' => $missing_width, 2470 'Widths' => $widthid, 2471 'FirstChar' => $firstChar, 2472 'LastChar' => $lastChar, 2473 'FontDescriptor' => $fontDescriptorId 2474 ); 2475 2476 if ($fbtype === 'ttf') { 2477 $tmp['SubType'] = 'TrueType'; 2478 } 2479 2480 $this->addMessage("adding extra info to font.($fontObj)"); 2481 2482 foreach ($tmp as $fk => $fv) { 2483 $this->addMessage("$fk : $fv"); 2484 } 2485 2486 $this->o_font($fontObj, 'add', $tmp); 2487 } else { 2488 $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts'); 2489 } 2490 2491 // also set the differences here, note that this means that these will take effect only the 2492 //first time that a font is selected, else they are ignored 2493 if (isset($options['differences'])) { 2494 $font['differences'] = $options['differences']; 2495 } 2496 } 2497 } 2498 2499 if ($set && isset($this->fonts[$fontName])) { 2500 // so if for some reason the font was not set in the last one then it will not be selected 2501 $this->currentBaseFont = $fontName; 2502 2503 // the next lines mean that if a new font is selected, then the current text state will be 2504 // applied to it as well. 2505 $this->currentFont = $this->currentBaseFont; 2506 $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; 2507 2508 //$this->setCurrentFont(); 2509 } 2510 2511 return $this->currentFontNum; 2512 //return $this->numObj; 2513 } 2514 2515 /** 2516 * sets up the current font, based on the font families, and the current text state 2517 * note that this system is quite flexible, a bold-italic font can be completely different to a 2518 * italic-bold font, and even bold-bold will have to be defined within the family to have meaning 2519 * This function is to be called whenever the currentTextState is changed, it will update 2520 * the currentFont setting to whatever the appropriatte family one is. 2521 * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont 2522 * This function will change the currentFont to whatever it should be, but will not change the 2523 * currentBaseFont. 2524 */ 2525 private function setCurrentFont() { 2526 // if (strlen($this->currentBaseFont) == 0){ 2527 // // then assume an initial font 2528 // $this->selectFont($this->defaultFont); 2529 // } 2530 // $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1); 2531 // if (strlen($this->currentTextState) 2532 // && isset($this->fontFamilies[$cf]) 2533 // && isset($this->fontFamilies[$cf][$this->currentTextState])){ 2534 // // then we are in some state or another 2535 // // and this font has a family, and the current setting exists within it 2536 // // select the font, then return it 2537 // $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState]; 2538 // $this->selectFont($nf,'',0); 2539 // $this->currentFont = $nf; 2540 // $this->currentFontNum = $this->fonts[$nf]['fontNum']; 2541 // } else { 2542 // // the this font must not have the right family member for the current state 2543 // // simply assume the base font 2544 $this->currentFont = $this->currentBaseFont; 2545 $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; 2546 // } 2547 } 2548 2549 /** 2550 * function for the user to find out what the ID is of the first page that was created during 2551 * startup - useful if they wish to add something to it later. 2552 */ 2553 function getFirstPageId() { 2554 return $this->firstPageId; 2555 } 2556 2557 /** 2558 * add content to the currently active object 2559 */ 2560 private function addContent($content) { 2561 $this->objects[$this->currentContents]['c'] .= $content; 2562 } 2563 2564 /** 2565 * sets the color for fill operations 2566 */ 2567 function setColor($color, $force = false) { 2568 $new_color = array($color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null); 2569 2570 if (!$force && $this->currentColor == $new_color) { 2571 return; 2572 } 2573 2574 if (isset($new_color[3])) { 2575 $this->currentColor = $new_color; 2576 $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F k", $this->currentColor)); 2577 } 2578 2579 else if (isset($new_color[2])) { 2580 $this->currentColor = $new_color; 2581 $this->addContent(vsprintf("\n%.3F %.3F %.3F rg", $this->currentColor)); 2582 } 2583 } 2584 2585 /** 2586 * sets the color for stroke operations 2587 */ 2588 function setStrokeColor($color, $force = false) { 2589 $new_color = array($color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null); 2590 2591 if (!$force && $this->currentStrokeColor == $new_color) return; 2592 2593 if (isset($new_color[3])) { 2594 $this->currentStrokeColor = $new_color; 2595 $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F K", $this->currentStrokeColor)); 2596 } 2597 2598 else if (isset($new_color[2])) { 2599 $this->currentStrokeColor = $new_color; 2600 $this->addContent(vsprintf("\n%.3F %.3F %.3F RG", $this->currentStrokeColor)); 2601 } 2602 } 2603 2604 /** 2605 * Set the graphics state for compositions 2606 */ 2607 function setGraphicsState($parameters) { 2608 // Create a new graphics state object 2609 // FIXME: should actually keep track of states that have already been created... 2610 $this->numObj++; 2611 $this->o_extGState($this->numObj, 'new', $parameters); 2612 $this->addContent("\n/GS$this->numStates gs"); 2613 } 2614 2615 /** 2616 * Set current blend mode & opacity for lines. 2617 * 2618 * Valid blend modes are: 2619 * 2620 * Normal, Multiply, Screen, Overlay, Darken, Lighten, 2621 * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, 2622 * Exclusion 2623 * 2624 * @param string $mode the blend mode to use 2625 * @param float $opacity 0.0 fully transparent, 1.0 fully opaque 2626 */ 2627 function setLineTransparency($mode, $opacity) { 2628 static $blend_modes = array("Normal", "Multiply", "Screen", 2629 "Overlay", "Darken", "Lighten", 2630 "ColorDogde", "ColorBurn", "HardLight", 2631 "SoftLight", "Difference", "Exclusion"); 2632 2633 if ( !in_array($mode, $blend_modes) ) 2634 $mode = "Normal"; 2635 2636 // Only create a new graphics state if required 2637 if ( $mode === $this->currentLineTransparency["mode"] && 2638 $opacity == $this->currentLineTransparency["opacity"] ) 2639 return; 2640 2641 $this->currentLineTransparency["mode"] = $mode; 2642 $this->currentLineTransparency["opacity"] = $opacity; 2643 2644 $options = array("BM" => "/$mode", 2645 "CA" => (float)$opacity); 2646 2647 $this->setGraphicsState($options); 2648 } 2649 2650 /** 2651 * Set current blend mode & opacity for filled objects. 2652 * 2653 * Valid blend modes are: 2654 * 2655 * Normal, Multiply, Screen, Overlay, Darken, Lighten, 2656 * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, 2657 * Exclusion 2658 * 2659 * @param string $mode the blend mode to use 2660 * @param float $opacity 0.0 fully transparent, 1.0 fully opaque 2661 */ 2662 function setFillTransparency($mode, $opacity) { 2663 static $blend_modes = array("Normal", "Multiply", "Screen", 2664 "Overlay", "Darken", "Lighten", 2665 "ColorDogde", "ColorBurn", "HardLight", 2666 "SoftLight", "Difference", "Exclusion"); 2667 2668 if ( !in_array($mode, $blend_modes) ) { 2669 $mode = "Normal"; 2670 } 2671 2672 if ( $mode === $this->currentFillTransparency["mode"] && 2673 $opacity == $this->currentFillTransparency["opacity"] ) { 2674 return; 2675 } 2676 2677 $this->currentFillTransparency["mode"] = $mode; 2678 $this->currentFillTransparency["opacity"] = $opacity; 2679 2680 $options = array( 2681 "BM" => "/$mode", 2682 "ca" => (float)$opacity, 2683 ); 2684 2685 $this->setGraphicsState($options); 2686 } 2687 2688 /** 2689 * draw a line from one set of coordinates to another 2690 */ 2691 function line($x1, $y1, $x2, $y2, $stroke = true) { 2692 $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F l", $x1, $y1, $x2, $y2)); 2693 2694 if ($stroke) { 2695 $this->addContent(' S'); 2696 } 2697 } 2698 2699 /** 2700 * draw a bezier curve based on 4 control points 2701 */ 2702 function curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) { 2703 // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points 2704 // as the control points for the curve. 2705 $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F %.3F %.3F %.3F %.3F c S", $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)); 2706 } 2707 2708 /** 2709 * draw a part of an ellipse 2710 */ 2711 function partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 = 0, $angle = 0, $nSeg = 8) { 2712 $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, false); 2713 } 2714 2715 /** 2716 * draw a filled ellipse 2717 */ 2718 function filledEllipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360) { 2719 return $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, true, true); 2720 } 2721 2722 /** 2723 * draw an ellipse 2724 * note that the part and filled ellipse are just special cases of this function 2725 * 2726 * draws an ellipse in the current line style 2727 * centered at $x0,$y0, radii $r1,$r2 2728 * if $r2 is not set, then a circle is drawn 2729 * from $astart to $afinish, measured in degrees, running anti-clockwise from the right hand side of the ellipse. 2730 * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a 2731 * pretty crappy shape at 2, as we are approximating with bezier curves. 2732 */ 2733 function ellipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360, $close = true, $fill = false, $stroke = true, $incomplete = false) { 2734 if ($r1 == 0) { 2735 return; 2736 } 2737 2738 if ($r2 == 0) { 2739 $r2 = $r1; 2740 } 2741 2742 if ($nSeg < 2) { 2743 $nSeg = 2; 2744 } 2745 2746 $astart = deg2rad((float)$astart); 2747 $afinish = deg2rad((float)$afinish); 2748 $totalAngle = $afinish-$astart; 2749 2750 $dt = $totalAngle/$nSeg; 2751 $dtm = $dt/3; 2752 2753 if ($angle != 0) { 2754 $a = -1*deg2rad((float)$angle); 2755 2756 $this->addContent(sprintf("\n q %.3F %.3F %.3F %.3F %.3F %.3F cm", cos($a), -sin($a), sin($a), cos($a), $x0, $y0)); 2757 2758 $x0 = 0; 2759 $y0 = 0; 2760 } 2761 2762 $t1 = $astart; 2763 $a0 = $x0 + $r1*cos($t1); 2764 $b0 = $y0 + $r2*sin($t1); 2765 $c0 = -$r1 * sin($t1); 2766 $d0 = $r2 * cos($t1); 2767 2768 if (!$incomplete) { 2769 $this->addContent(sprintf("\n%.3F %.3F m ", $a0, $b0)); 2770 } 2771 2772 for ($i = 1; $i <= $nSeg; $i++) { 2773 // draw this bit of the total curve 2774 $t1 = $i * $dt + $astart; 2775 $a1 = $x0 + $r1 * cos($t1); 2776 $b1 = $y0 + $r2 * sin($t1); 2777 $c1 = -$r1 * sin($t1); 2778 $d1 = $r2 * cos($t1); 2779 2780 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F %.3F %.3F c", ($a0+$c0*$dtm), ($b0+$d0*$dtm), ($a1-$c1*$dtm), ($b1-$d1*$dtm), $a1, $b1)); 2781 2782 $a0 = $a1; 2783 $b0 = $b1; 2784 $c0 = $c1; 2785 $d0 = $d1; 2786 } 2787 2788 if (!$incomplete) { 2789 if ($fill) { 2790 $this->addContent(' f'); 2791 } 2792 else if ($close) { 2793 $this->addContent(' s'); // small 's' signifies closing the path as well 2794 } 2795 else if ($stroke) { 2796 $this->addContent(' S'); 2797 } 2798 } 2799 2800 if ($angle != 0) { 2801 $this->addContent(' Q'); 2802 } 2803 } 2804 2805 /** 2806 * this sets the line drawing style. 2807 * width, is the thickness of the line in user units 2808 * cap is the type of cap to put on the line, values can be 'butt','round','square' 2809 * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the 2810 * end of the line. 2811 * join can be 'miter', 'round', 'bevel' 2812 * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the 2813 * on and off dashes. 2814 * (2) represents 2 on, 2 off, 2 on , 2 off ... 2815 * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc 2816 * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. 2817 */ 2818 function setLineStyle($width = 1, $cap = '', $join = '', $dash = '', $phase = 0) { 2819 // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day 2820 $string = ''; 2821 2822 if ($width > 0) { 2823 $string.= "$width w"; 2824 } 2825 2826 $ca = array('butt' => 0, 'round' => 1, 'square' => 2); 2827 2828 if (isset($ca[$cap])) { 2829 $string.= " $ca[$cap] J"; 2830 } 2831 2832 $ja = array('miter' => 0, 'round' => 1, 'bevel' => 2); 2833 2834 if (isset($ja[$join])) { 2835 $string.= " $ja[$join] j"; 2836 } 2837 2838 if (is_array($dash)) { 2839 $string.= ' [ ' . implode(' ', $dash) . " ] $phase d"; 2840 } 2841 2842 $this->currentLineStyle = $string; 2843 $this->addContent("\n$string"); 2844 } 2845 2846 /** 2847 * draw a polygon, the syntax for this is similar to the GD polygon command 2848 */ 2849 function polygon($p, $np, $f = false) { 2850 $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1])); 2851 2852 for ($i = 2; $i < $np * 2; $i = $i + 2) { 2853 $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i+1])); 2854 } 2855 2856 if ($f) { 2857 $this->addContent(' f'); 2858 } else { 2859 $this->addContent(' S'); 2860 } 2861 } 2862 2863 /** 2864 * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not 2865 * the coordinates of the upper-right corner 2866 */ 2867 function filledRectangle($x1, $y1, $width, $height) { 2868 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re f", $x1, $y1, $width, $height)); 2869 } 2870 2871 /** 2872 * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not 2873 * the coordinates of the upper-right corner 2874 */ 2875 function rectangle($x1, $y1, $width, $height) { 2876 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re S", $x1, $y1, $width, $height)); 2877 } 2878 2879 /** 2880 * save the current graphic state 2881 */ 2882 function save() { 2883 // we must reset the color cache or it will keep bad colors after clipping 2884 $this->currentColor = null; 2885 $this->currentStrokeColor = null; 2886 $this->addContent("\nq"); 2887 } 2888 2889 /** 2890 * restore the last graphic state 2891 */ 2892 function restore() { 2893 // we must reset the color cache or it will keep bad colors after clipping 2894 $this->currentColor = null; 2895 $this->currentStrokeColor = null; 2896 $this->addContent("\nQ"); 2897 } 2898 2899 /** 2900 * draw a clipping rectangle, all the elements added after this will be clipped 2901 */ 2902 function clippingRectangle($x1, $y1, $width, $height) { 2903 $this->save(); 2904 $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re W n", $x1, $y1, $width, $height)); 2905 } 2906 2907 /** 2908 * draw a clipping rounded rectangle, all the elements added after this will be clipped 2909 */ 2910 function clippingRectangleRounded($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) { 2911 $this->save(); 2912 2913 // start: top edge, left end 2914 $this->addContent(sprintf("\n%.3F %.3F m ", $x1, $y1 - $rTL + $h)); 2915 2916 // curve: bottom-left corner 2917 $this->ellipse($x1 + $rBL, $y1 + $rBL, $rBL, 0, 0, 8, 180, 270, false, false, false, true); 2918 2919 // line: right edge, bottom end 2920 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w - $rBR, $y1)); 2921 2922 // curve: bottom-right corner 2923 $this->ellipse($x1 + $w - $rBR, $y1 + $rBR, $rBR, 0, 0, 8, 270, 360, false, false, false, true); 2924 2925 // line: right edge, top end 2926 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w, $y1 + $h - $rTR)); 2927 2928 // curve: bottom-right corner 2929 $this->ellipse($x1 + $w - $rTR, $y1 + $h - $rTR, $rTR, 0, 0, 8, 0, 90, false, false, false, true); 2930 2931 // line: bottom edge, right end 2932 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rTL, $y1 + $h)); 2933 2934 // curve: top-right corner 2935 $this->ellipse($x1 + $rTL, $y1 + $h - $rTL, $rTL, 0, 0, 8, 90, 180, false, false, false, true); 2936 2937 // line: top edge, left end 2938 $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rBL, $y1)); 2939 2940 // Close & clip 2941 $this->addContent(" W n"); 2942 } 2943 2944 /** 2945 * ends the last clipping shape 2946 */ 2947 function clippingEnd() { 2948 $this->restore(); 2949 } 2950 2951 /** 2952 * scale 2953 * @param float $s_x scaling factor for width as percent 2954 * @param float $s_y scaling factor for height as percent 2955 * @param float $x Origin abscisse 2956 * @param float $y Origin ordinate 2957 */ 2958 function scale($s_x, $s_y, $x, $y) { 2959 $y = $this->currentPageSize["height"] - $y; 2960 2961 $tm = array( 2962 $s_x, 0, 2963 0, $s_y, 2964 $x*(1-$s_x), $y*(1-$s_y) 2965 ); 2966 2967 $this->transform($tm); 2968 } 2969 2970 /** 2971 * translate 2972 * @param float $t_x movement to the right 2973 * @param float $t_y movement to the bottom 2974 */ 2975 function translate($t_x, $t_y) { 2976 $tm = array( 2977 1, 0, 2978 0, 1, 2979 $t_x, -$t_y 2980 ); 2981 2982 $this->transform($tm); 2983 } 2984 2985 /** 2986 * rotate 2987 * @param float $angle angle in degrees for counter-clockwise rotation 2988 * @param float $x Origin abscisse 2989 * @param float $y Origin ordinate 2990 */ 2991 function rotate($angle, $x, $y) { 2992 $y = $this->currentPageSize["height"] - $y; 2993 2994 $a = deg2rad($angle); 2995 $cos_a = cos($a); 2996 $sin_a = sin($a); 2997 2998 $tm = array( 2999 $cos_a, -$sin_a, 3000 $sin_a, $cos_a, 3001 $x - $sin_a*$y - $cos_a*$x, $y - $cos_a*$y + $sin_a*$x, 3002 ); 3003 3004 $this->transform($tm); 3005 } 3006 3007 /** 3008 * skew 3009 * @param float $angle_x 3010 * @param float $angle_y 3011 * @param float $x Origin abscisse 3012 * @param float $y Origin ordinate 3013 */ 3014 function skew($angle_x, $angle_y, $x, $y) { 3015 $y = $this->currentPageSize["height"] - $y; 3016 3017 $tan_x = tan(deg2rad($angle_x)); 3018 $tan_y = tan(deg2rad($angle_y)); 3019 3020 $tm = array( 3021 1, -$tan_y, 3022 -$tan_x, 1, 3023 $tan_x*$y, $tan_y*$x, 3024 ); 3025 3026 $this->transform($tm); 3027 } 3028 3029 /** 3030 * apply graphic transformations 3031 * @param array $tm transformation matrix 3032 */ 3033 function transform($tm) { 3034 $this->addContent(vsprintf("\n %.3F %.3F %.3F %.3F %.3F %.3F cm", $tm)); 3035 } 3036 3037 /** 3038 * add a new page to the document 3039 * this also makes the new page the current active object 3040 */ 3041 function newPage($insert = 0, $id = 0, $pos = 'after') { 3042 // if there is a state saved, then go up the stack closing them 3043 // then on the new page, re-open them with the right setings 3044 3045 if ($this->nStateStack) { 3046 for ($i = $this->nStateStack; $i >= 1; $i--) { 3047 $this->restoreState($i); 3048 } 3049 } 3050 3051 $this->numObj++; 3052 3053 if ($insert) { 3054 // the id from the ezPdf class is the id of the contents of the page, not the page object itself 3055 // query that object to find the parent 3056 $rid = $this->objects[$id]['onPage']; 3057 $opt = array('rid' => $rid, 'pos' => $pos); 3058 $this->o_page($this->numObj, 'new', $opt); 3059 } else { 3060 $this->o_page($this->numObj, 'new'); 3061 } 3062 3063 // if there is a stack saved, then put that onto the page 3064 if ($this->nStateStack) { 3065 for ($i = 1; $i <= $this->nStateStack; $i++) { 3066 $this->saveState($i); 3067 } 3068 } 3069 3070 // and if there has been a stroke or fill color set, then transfer them 3071 if (isset($this->currentColor)) { 3072 $this->setColor($this->currentColor, true); 3073 } 3074 3075 if (isset($this->currentStrokeColor)) { 3076 $this->setStrokeColor($this->currentStrokeColor, true); 3077 } 3078 3079 // if there is a line style set, then put this in too 3080 if (mb_strlen($this->currentLineStyle, '8bit')) { 3081 $this->addContent("\n$this->currentLineStyle"); 3082 } 3083 3084 // the call to the o_page object set currentContents to the present page, so this can be returned as the page id 3085 return $this->currentContents; 3086 } 3087 3088 /** 3089 * output the pdf code, streaming it to the browser 3090 * the relevant headers are set so that hopefully the browser will recognise it 3091 */ 3092 function stream($options = '') { 3093 // setting the options allows the adjustment of the headers 3094 // values at the moment are: 3095 // 'Content-Disposition' => 'filename' - sets the filename, though not too sure how well this will 3096 // work as in my trial the browser seems to use the filename of the php file with .pdf on the end 3097 // 'Accept-Ranges' => 1 or 0 - if this is not set to 1, then this header is not included, off by default 3098 // this header seems to have caused some problems despite tha fact that it is supposed to solve 3099 // them, so I am leaving it off by default. 3100 // 'compress' = > 1 or 0 - apply content stream compression, this is on (1) by default 3101 // 'Attachment' => 1 or 0 - if 1, force the browser to open a download dialog 3102 if (!is_array($options)) { 3103 $options = array(); 3104 } 3105 3106 if ( headers_sent()) { 3107 die("Unable to stream pdf: headers already sent"); 3108 } 3109 3110 $debug = empty($options['compression']); 3111 $tmp = ltrim($this->output($debug)); 3112 3113 header("Cache-Control: private"); 3114 header("Content-type: application/pdf"); 3115 3116 //FIXME: I don't know that this is sufficient for determining content length (i.e. what about transport compression?) 3117 header("Content-Length: " . mb_strlen($tmp, '8bit')); 3118 $fileName = (isset($options['Content-Disposition']) ? $options['Content-Disposition'] : 'file.pdf'); 3119 3120 if ( !isset($options["Attachment"])) 3121 $options["Attachment"] = true; 3122 3123 $attachment = $options["Attachment"] ? "attachment" : "inline"; 3124 3125 header("Content-Disposition: $attachment; filename=\"$fileName\""); 3126 3127 if (isset($options['Accept-Ranges']) && $options['Accept-Ranges'] == 1) { 3128 //FIXME: Is this the correct value ... spec says 1#range-unit 3129 header("Accept-Ranges: " . mb_strlen($tmp, '8bit')); 3130 } 3131 3132 echo $tmp; 3133 flush(); 3134 } 3135 3136 /** 3137 * return the height in units of the current font in the given size 3138 */ 3139 function getFontHeight($size) { 3140 if (!$this->numFonts) { 3141 $this->selectFont($this->defaultFont); 3142 } 3143 3144 $font = $this->fonts[$this->currentFont]; 3145 3146 // for the current font, and the given size, what is the height of the font in user units 3147 if ( isset($font['Ascender']) && isset($font['Descender']) ) { 3148 $h = $font['Ascender']-$font['Descender']; 3149 } 3150 else { 3151 $h = $font['FontBBox'][3]-$font['FontBBox'][1]; 3152 } 3153 3154 // have to adjust by a font offset for Windows fonts. unfortunately it looks like 3155 // the bounding box calculations are wrong and I don't know why. 3156 if (isset($font['FontHeightOffset'])) { 3157 // For CourierNew from Windows this needs to be -646 to match the 3158 // Adobe native Courier font. 3159 // 3160 // For FreeMono from GNU this needs to be -337 to match the 3161 // Courier font. 3162 // 3163 // Both have been added manually to the .afm and .ufm files. 3164 $h += (int)$font['FontHeightOffset']; 3165 } 3166 3167 return $size*$h/1000; 3168 } 3169 3170 function getFontXHeight($size) { 3171 if (!$this->numFonts) { 3172 $this->selectFont($this->defaultFont); 3173 } 3174 3175 $font = $this->fonts[$this->currentFont]; 3176 3177 // for the current font, and the given size, what is the height of the font in user units 3178 if ( isset($font['XHeight']) ) { 3179 $xh = $font['Ascender']-$font['Descender']; 3180 } 3181 else { 3182 $xh = $this->getFontHeight($size) / 2; 3183 } 3184 3185 return $size*$xh/1000; 3186 } 3187 3188 /** 3189 * return the font descender, this will normally return a negative number 3190 * if you add this number to the baseline, you get the level of the bottom of the font 3191 * it is in the pdf user units 3192 */ 3193 function getFontDescender($size) { 3194 // note that this will most likely return a negative value 3195 if (!$this->numFonts) { 3196 $this->selectFont($this->defaultFont); 3197 } 3198 3199 //$h = $this->fonts[$this->currentFont]['FontBBox'][1]; 3200 $h = $this->fonts[$this->currentFont]['Descender']; 3201 3202 return $size*$h/1000; 3203 } 3204 3205 /** 3206 * filter the text, this is applied to all text just before being inserted into the pdf document 3207 * it escapes the various things that need to be escaped, and so on 3208 * 3209 * @access private 3210 */ 3211 function filterText($text, $bom = true, $convert_encoding = true) { 3212 if (!$this->numFonts) { 3213 $this->selectFont($this->defaultFont); 3214 } 3215 3216 if ($convert_encoding) { 3217 $cf = $this->currentFont; 3218 if (isset($this->fonts[$cf]) && $this->fonts[$cf]['isUnicode']) { 3219 //$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); 3220 $text = $this->utf8toUtf16BE($text, $bom); 3221 } else { 3222 //$text = html_entity_decode($text, ENT_QUOTES); 3223 $text = mb_convert_encoding($text, self::$targetEncoding, 'UTF-8'); 3224 } 3225 } 3226 3227 // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290) 3228 return strtr($text, array(')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r')); 3229 } 3230 3231 /** 3232 * return array containing codepoints (UTF-8 character values) for the 3233 * string passed in. 3234 * 3235 * based on the excellent TCPDF code by Nicola Asuni and the 3236 * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html 3237 * 3238 * @access private 3239 * @author Orion Richardson 3240 * @since January 5, 2008 3241 * @param string $text UTF-8 string to process 3242 * @return array UTF-8 codepoints array for the string 3243 */ 3244 function utf8toCodePointsArray(&$text) { 3245 $length = mb_strlen($text, '8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040 3246 $unicode = array(); // array containing unicode values 3247 $bytes = array(); // array containing single character byte sequences 3248 $numbytes = 1; // number of octetc needed to represent the UTF-8 character 3249 3250 for ($i = 0; $i < $length; $i++) { 3251 $c = ord($text[$i]); // get one string character at time 3252 if (count($bytes) === 0) { // get starting octect 3253 if ($c <= 0x7F) { 3254 $unicode[] = $c; // use the character "as is" because is ASCII 3255 $numbytes = 1; 3256 } elseif (($c >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN) 3257 $bytes[] = ($c - 0xC0) << 0x06; 3258 $numbytes = 2; 3259 } elseif (($c >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN) 3260 $bytes[] = ($c - 0xE0) << 0x0C; 3261 $numbytes = 3; 3262 } elseif (($c >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN) 3263 $bytes[] = ($c - 0xF0) << 0x12; 3264 $numbytes = 4; 3265 } else { 3266 // use replacement character for other invalid sequences 3267 $unicode[] = 0xFFFD; 3268 $bytes = array(); 3269 $numbytes = 1; 3270 } 3271 } elseif (($c >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN 3272 $bytes[] = $c - 0x80; 3273 if (count($bytes) === $numbytes) { 3274 // compose UTF-8 bytes to a single unicode value 3275 $c = $bytes[0]; 3276 for ($j = 1; $j < $numbytes; $j++) { 3277 $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06)); 3278 } 3279 if ((($c >= 0xD800) AND ($c <= 0xDFFF)) OR ($c >= 0x10FFFF)) { 3280 // The definition of UTF-8 prohibits encoding character numbers between 3281 // U+D800 and U+DFFF, which are reserved for use with the UTF-16 3282 // encoding form (as surrogate pairs) and do not directly represent 3283 // characters. 3284 $unicode[] = 0xFFFD; // use replacement character 3285 } else { 3286 $unicode[] = $c; // add char to array 3287 } 3288 // reset data for next char 3289 $bytes = array(); 3290 $numbytes = 1; 3291 } 3292 } else { 3293 // use replacement character for other invalid sequences 3294 $unicode[] = 0xFFFD; 3295 $bytes = array(); 3296 $numbytes = 1; 3297 } 3298 } 3299 return $unicode; 3300 } 3301 3302 /** 3303 * convert UTF-8 to UTF-16 with an additional byte order marker 3304 * at the front if required. 3305 * 3306 * based on the excellent TCPDF code by Nicola Asuni and the 3307 * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html 3308 * 3309 * @access private 3310 * @author Orion Richardson 3311 * @since January 5, 2008 3312 * @param string $text UTF-8 string to process 3313 * @param boolean $bom whether to add the byte order marker 3314 * @return string UTF-16 result string 3315 */ 3316 function utf8toUtf16BE(&$text, $bom = true) { 3317 $cf = $this->currentFont; 3318 if (!$this->fonts[$cf]['isUnicode']) return $text; 3319 $out = $bom ? "\xFE\xFF" : ''; 3320 3321 $unicode = $this->utf8toCodePointsArray($text); 3322 foreach ($unicode as $c) { 3323 if ($c === 0xFFFD) { 3324 $out .= "\xFF\xFD"; // replacement character 3325 } elseif ($c < 0x10000) { 3326 $out .= chr($c >> 0x08) . chr($c & 0xFF); 3327 } else { 3328 $c -= 0x10000; 3329 $w1 = 0xD800 | ($c >> 0x10); 3330 $w2 = 0xDC00 | ($c & 0x3FF); 3331 $out .= chr($w1 >> 0x08) . chr($w1 & 0xFF) . chr($w2 >> 0x08) . chr($w2 & 0xFF); 3332 } 3333 } 3334 return $out; 3335 } 3336 3337 /** 3338 * given a start position and information about how text is to be laid out, calculate where 3339 * on the page the text will end 3340 */ 3341 private function getTextPosition($x, $y, $angle, $size, $wa, $text) { 3342 // given this information return an array containing x and y for the end position as elements 0 and 1 3343 $w = $this->getTextWidth($size, $text); 3344 3345 // need to adjust for the number of spaces in this text 3346 $words = explode(' ', $text); 3347 $nspaces = count($words) -1; 3348 $w+= $wa*$nspaces; 3349 $a = deg2rad((float)$angle); 3350 3351 return array(cos($a) *$w+$x, -sin($a) *$w+$y); 3352 } 3353 3354 /** 3355 * Callback method used by smallCaps 3356 * 3357 * @param array $matches 3358 * @return string 3359 */ 3360 function toUpper($matches) { 3361 return mb_strtoupper($matches[0]); 3362 } 3363 3364 function concatMatches($matches) { 3365 $str = ""; 3366 foreach ($matches as $match){ 3367 $str .= $match[0]; 3368 } 3369 return $str; 3370 } 3371 3372 /** 3373 * add text to the document, at a specified location, size and angle on the page 3374 */ 3375 function registerText($font, $text) { 3376 if ( !$this->isUnicode || in_array(mb_strtolower(basename($font)), self::$coreFonts) ) { 3377 return; 3378 } 3379 3380 if ( !isset($this->stringSubsets[$font]) ) { 3381 $this->stringSubsets[$font] = array(); 3382 } 3383 3384 $this->stringSubsets[$font] = array_unique(array_merge($this->stringSubsets[$font], $this->utf8toCodePointsArray($text))); 3385 } 3386 3387 /** 3388 * add text to the document, at a specified location, size and angle on the page 3389 */ 3390 function addText($x, $y, $size, $text, $angle = 0, $wordSpaceAdjust = 0, $charSpaceAdjust = 0, $smallCaps = false) { 3391 if (!$this->numFonts) { 3392 $this->selectFont($this->defaultFont); 3393 } 3394 3395 $text = str_replace(array("\r", "\n"), "", $text); 3396 3397 if ( $smallCaps ) { 3398 preg_match_all("/(\P{Ll}+)/u", $text, $matches, PREG_SET_ORDER); 3399 $lower = $this->concatMatches($matches); 3400 d($lower); 3401 3402 preg_match_all("/(\p{Ll}+)/u", $text, $matches, PREG_SET_ORDER); 3403 $other = $this->concatMatches($matches); 3404 d($other); 3405 3406 //$text = preg_replace_callback("/\p{Ll}/u", array($this, "toUpper"), $text); 3407 } 3408 3409 // if there are any open callbacks, then they should be called, to show the start of the line 3410 if ($this->nCallback > 0) { 3411 for ($i = $this->nCallback; $i > 0; $i--) { 3412 // call each function 3413 $info = array( 3414 'x' => $x, 3415 'y' => $y, 3416 'angle' => $angle, 3417 'status' => 'sol', 3418 'p' => $this->callback[$i]['p'], 3419 'nCallback' => $this->callback[$i]['nCallback'], 3420 'height' => $this->callback[$i]['height'], 3421 'descender' => $this->callback[$i]['descender'] 3422 ); 3423 3424 $func = $this->callback[$i]['f']; 3425 $this->$func($info); 3426 } 3427 } 3428 3429 if ($angle == 0) { 3430 $this->addContent(sprintf("\nBT %.3F %.3F Td", $x, $y)); 3431 } else { 3432 $a = deg2rad((float)$angle); 3433 $this->addContent(sprintf("\nBT %.3F %.3F %.3F %.3F %.3F %.3F Tm", cos($a), -sin($a), sin($a), cos($a), $x, $y)); 3434 } 3435 3436 if ($wordSpaceAdjust != 0 || $wordSpaceAdjust != $this->wordSpaceAdjust) { 3437 $this->wordSpaceAdjust = $wordSpaceAdjust; 3438 $this->addContent(sprintf(" %.3F Tw", $wordSpaceAdjust)); 3439 } 3440 3441 if ($charSpaceAdjust != 0 || $charSpaceAdjust != $this->charSpaceAdjust) { 3442 $this->charSpaceAdjust = $charSpaceAdjust; 3443 $this->addContent(sprintf(" %.3F Tc", $charSpaceAdjust)); 3444 } 3445 3446 $len = mb_strlen($text); 3447 $start = 0; 3448 3449 if ($start < $len) { 3450 $part = $text; // OAR - Don't need this anymore, given that $start always equals zero. substr($text, $start); 3451 $place_text = $this->filterText($part, false); 3452 // modify unicode text so that extra word spacing is manually implemented (bug #) 3453 $cf = $this->currentFont; 3454 if ($this->fonts[$cf]['isUnicode'] && $wordSpaceAdjust != 0) { 3455 $space_scale = 1000 / $size; 3456 //$place_text = str_replace(' ', ') ( ) '.($this->getTextWidth($size, chr(32), $wordSpaceAdjust)*-75).' (', $place_text); 3457 $place_text = str_replace(' ', ' ) '.(-round($space_scale*$wordSpaceAdjust)).' (', $place_text); 3458 } 3459 $this->addContent(" /F$this->currentFontNum ".sprintf('%.1F Tf ', $size)); 3460 $this->addContent(" [($place_text)] TJ"); 3461 } 3462 3463 $this->addContent(' ET'); 3464 3465 // if there are any open callbacks, then they should be called, to show the end of the line 3466 if ($this->nCallback > 0) { 3467 for ($i = $this->nCallback; $i > 0; $i--) { 3468 // call each function 3469 $tmp = $this->getTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text); 3470 $info = array( 3471 'x' => $tmp[0], 3472 'y' => $tmp[1], 3473 'angle' => $angle, 3474 'status' => 'eol', 3475 'p' => $this->callback[$i]['p'], 3476 'nCallback' => $this->callback[$i]['nCallback'], 3477 'height' => $this->callback[$i]['height'], 3478 'descender' => $this->callback[$i]['descender'] 3479 ); 3480 $func = $this->callback[$i]['f']; 3481 $this->$func($info); 3482 } 3483 } 3484 } 3485 3486 /** 3487 * calculate how wide a given text string will be on a page, at a given size. 3488 * this can be called externally, but is alse used by the other class functions 3489 */ 3490 function getTextWidth($size, $text, $word_spacing = 0, $char_spacing = 0) { 3491 static $ord_cache = array(); 3492 3493 // this function should not change any of the settings, though it will need to 3494 // track any directives which change during calculation, so copy them at the start 3495 // and put them back at the end. 3496 $store_currentTextState = $this->currentTextState; 3497 3498 if (!$this->numFonts) { 3499 $this->selectFont($this->defaultFont); 3500 } 3501 3502 $text = str_replace(array("\r", "\n"), "", $text); 3503 3504 // converts a number or a float to a string so it can get the width 3505 $text = "$text"; 3506 3507 // hmm, this is where it all starts to get tricky - use the font information to 3508 // calculate the width of each character, add them up and convert to user units 3509 $w = 0; 3510 $cf = $this->currentFont; 3511 $current_font = $this->fonts[$cf]; 3512 $space_scale = 1000 / $size; 3513 $n_spaces = 0; 3514 3515 if ( $current_font['isUnicode']) { 3516 // for Unicode, use the code points array to calculate width rather 3517 // than just the string itself 3518 $unicode = $this->utf8toCodePointsArray($text); 3519 3520 foreach ($unicode as $char) { 3521 // check if we have to replace character 3522 if ( isset($current_font['differences'][$char])) { 3523 $char = $current_font['differences'][$char]; 3524 } 3525 3526 if ( isset($current_font['C'][$char]) ) { 3527 $char_width = $current_font['C'][$char]; 3528 3529 // add the character width 3530 $w += $char_width; 3531 3532 // add additional padding for space 3533 if ( isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space' ) { // Space 3534 $w += $word_spacing * $space_scale; 3535 $n_spaces++; 3536 } 3537 } 3538 } 3539 3540 // add additionnal char spacing 3541 if ( $char_spacing != 0 ) { 3542 $w += $char_spacing * $space_scale * (count($unicode) + $n_spaces); 3543 } 3544 3545 } else { 3546 // If CPDF is in Unicode mode but the current font does not support Unicode we need to convert the character set to Windows-1252 3547 if ( $this->isUnicode ) { 3548 $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8'); 3549 } 3550 3551 $len = mb_strlen($text, 'Windows-1252'); 3552 3553 for ($i = 0; $i < $len; $i++) { 3554 $c = $text[$i]; 3555 $char = isset($ord_cache[$c]) ? $ord_cache[$c] : ($ord_cache[$c] = ord($c)); 3556 3557 // check if we have to replace character 3558 if ( isset($current_font['differences'][$char])) { 3559 $char = $current_font['differences'][$char]; 3560 } 3561 3562 if ( isset($current_font['C'][$char]) ) { 3563 $char_width = $current_font['C'][$char]; 3564 3565 // add the character width 3566 $w += $char_width; 3567 3568 // add additional padding for space 3569 if ( isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space' ) { // Space 3570 $w += $word_spacing * $space_scale; 3571 $n_spaces++; 3572 } 3573 } 3574 } 3575 3576 // add additionnal char spacing 3577 if ( $char_spacing != 0 ) { 3578 $w += $char_spacing * $space_scale * ($len + $n_spaces); 3579 } 3580 } 3581 3582 $this->currentTextState = $store_currentTextState; 3583 $this->setCurrentFont(); 3584 3585 return $w*$size/1000; 3586 } 3587 3588 /** 3589 * this will be called at a new page to return the state to what it was on the 3590 * end of the previous page, before the stack was closed down 3591 * This is to get around not being able to have open 'q' across pages 3592 * 3593 */ 3594 function saveState($pageEnd = 0) { 3595 if ($pageEnd) { 3596 // this will be called at a new page to return the state to what it was on the 3597 // end of the previous page, before the stack was closed down 3598 // This is to get around not being able to have open 'q' across pages 3599 $opt = $this->stateStack[$pageEnd]; 3600 // ok to use this as stack starts numbering at 1 3601 $this->setColor($opt['col'], true); 3602 $this->setStrokeColor($opt['str'], true); 3603 $this->addContent("\n".$opt['lin']); 3604 // $this->currentLineStyle = $opt['lin']; 3605 } else { 3606 $this->nStateStack++; 3607 $this->stateStack[$this->nStateStack] = array( 3608 'col' => $this->currentColor, 3609 'str' => $this->currentStrokeColor, 3610 'lin' => $this->currentLineStyle 3611 ); 3612 } 3613 3614 $this->save(); 3615 } 3616 3617 /** 3618 * restore a previously saved state 3619 */ 3620 function restoreState($pageEnd = 0) { 3621 if (!$pageEnd) { 3622 $n = $this->nStateStack; 3623 $this->currentColor = $this->stateStack[$n]['col']; 3624 $this->currentStrokeColor = $this->stateStack[$n]['str']; 3625 $this->addContent("\n".$this->stateStack[$n]['lin']); 3626 $this->currentLineStyle = $this->stateStack[$n]['lin']; 3627 $this->stateStack[$n] = null; 3628 unset($this->stateStack[$n]); 3629 $this->nStateStack--; 3630 } 3631 3632 $this->restore(); 3633 } 3634 3635 /** 3636 * make a loose object, the output will go into this object, until it is closed, then will revert to 3637 * the current one. 3638 * this object will not appear until it is included within a page. 3639 * the function will return the object number 3640 */ 3641 function openObject() { 3642 $this->nStack++; 3643 $this->stack[$this->nStack] = array('c' => $this->currentContents, 'p' => $this->currentPage); 3644 // add a new object of the content type, to hold the data flow 3645 $this->numObj++; 3646 $this->o_contents($this->numObj, 'new'); 3647 $this->currentContents = $this->numObj; 3648 $this->looseObjects[$this->numObj] = 1; 3649 3650 return $this->numObj; 3651 } 3652 3653 /** 3654 * open an existing object for editing 3655 */ 3656 function reopenObject($id) { 3657 $this->nStack++; 3658 $this->stack[$this->nStack] = array('c' => $this->currentContents, 'p' => $this->currentPage); 3659 $this->currentContents = $id; 3660 3661 // also if this object is the primary contents for a page, then set the current page to its parent 3662 if (isset($this->objects[$id]['onPage'])) { 3663 $this->currentPage = $this->objects[$id]['onPage']; 3664 } 3665 } 3666 3667 /** 3668 * close an object 3669 */ 3670 function closeObject() { 3671 // close the object, as long as there was one open in the first place, which will be indicated by 3672 // an objectId on the stack. 3673 if ($this->nStack > 0) { 3674 $this->currentContents = $this->stack[$this->nStack]['c']; 3675 $this->currentPage = $this->stack[$this->nStack]['p']; 3676 $this->nStack--; 3677 // easier to probably not worry about removing the old entries, they will be overwritten 3678 // if there are new ones. 3679 } 3680 } 3681 3682 /** 3683 * stop an object from appearing on pages from this point on 3684 */ 3685 function stopObject($id) { 3686 // if an object has been appearing on pages up to now, then stop it, this page will 3687 // be the last one that could contian it. 3688 if (isset($this->addLooseObjects[$id])) { 3689 $this->addLooseObjects[$id] = ''; 3690 } 3691 } 3692 3693 /** 3694 * after an object has been created, it wil only show if it has been added, using this function. 3695 */ 3696 function addObject($id, $options = 'add') { 3697 // add the specified object to the page 3698 if (isset($this->looseObjects[$id]) && $this->currentContents != $id) { 3699 // then it is a valid object, and it is not being added to itself 3700 switch ($options) { 3701 case 'all': 3702 // then this object is to be added to this page (done in the next block) and 3703 // all future new pages. 3704 $this->addLooseObjects[$id] = 'all'; 3705 3706 case 'add': 3707 if (isset($this->objects[$this->currentContents]['onPage'])) { 3708 // then the destination contents is the primary for the page 3709 // (though this object is actually added to that page) 3710 $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id); 3711 } 3712 break; 3713 3714 case 'even': 3715 $this->addLooseObjects[$id] = 'even'; 3716 $pageObjectId = $this->objects[$this->currentContents]['onPage']; 3717 if ($this->objects[$pageObjectId]['info']['pageNum']%2 == 0) { 3718 $this->addObject($id); 3719 // hacky huh :) 3720 } 3721 break; 3722 3723 case 'odd': 3724 $this->addLooseObjects[$id] = 'odd'; 3725 $pageObjectId = $this->objects[$this->currentContents]['onPage']; 3726 if ($this->objects[$pageObjectId]['info']['pageNum']%2 == 1) { 3727 $this->addObject($id); 3728 // hacky huh :) 3729 } 3730 break; 3731 3732 case 'next': 3733 $this->addLooseObjects[$id] = 'all'; 3734 break; 3735 3736 case 'nexteven': 3737 $this->addLooseObjects[$id] = 'even'; 3738 break; 3739 3740 case 'nextodd': 3741 $this->addLooseObjects[$id] = 'odd'; 3742 break; 3743 } 3744 } 3745 } 3746 3747 /** 3748 * return a storable representation of a specific object 3749 */ 3750 function serializeObject($id) { 3751 if ( array_key_exists($id, $this->objects)) { 3752 return serialize($this->objects[$id]); 3753 } 3754 } 3755 3756 /** 3757 * restore an object from its stored representation. returns its new object id. 3758 */ 3759 function restoreSerializedObject($obj) { 3760 $obj_id = $this->openObject(); 3761 $this->objects[$obj_id] = unserialize($obj); 3762 $this->closeObject(); 3763 return $obj_id; 3764 } 3765 3766 /** 3767 * add content to the documents info object 3768 */ 3769 function addInfo($label, $value = 0) { 3770 // this will only work if the label is one of the valid ones. 3771 // modify this so that arrays can be passed as well. 3772 // if $label is an array then assume that it is key => value pairs 3773 // else assume that they are both scalar, anything else will probably error 3774 if (is_array($label)) { 3775 foreach ($label as $l => $v) { 3776 $this->o_info($this->infoObject, $l, $v); 3777 } 3778 } else { 3779 $this->o_info($this->infoObject, $label, $value); 3780 } 3781 } 3782 3783 /** 3784 * set the viewer preferences of the document, it is up to the browser to obey these. 3785 */ 3786 function setPreferences($label, $value = 0) { 3787 // this will only work if the label is one of the valid ones. 3788 if (is_array($label)) { 3789 foreach ($label as $l => $v) { 3790 $this->o_catalog($this->catalogId, 'viewerPreferences', array($l => $v)); 3791 } 3792 } else { 3793 $this->o_catalog($this->catalogId, 'viewerPreferences', array($label => $value)); 3794 } 3795 } 3796 3797 /** 3798 * extract an integer from a position in a byte stream 3799 */ 3800 private function getBytes(&$data, $pos, $num) { 3801 // return the integer represented by $num bytes from $pos within $data 3802 $ret = 0; 3803 for ($i = 0; $i < $num; $i++) { 3804 $ret *= 256; 3805 $ret += ord($data[$pos+$i]); 3806 } 3807 3808 return $ret; 3809 } 3810 3811 /** 3812 * Check if image already added to pdf image directory. 3813 * If yes, need not to create again (pass empty data) 3814 */ 3815 function image_iscached($imgname) { 3816 return isset($this->imagelist[$imgname]); 3817 } 3818 3819 /** 3820 * add a PNG image into the document, from a GD object 3821 * this should work with remote files 3822 * 3823 * @param string $file The PNG file 3824 * @param float $x X position 3825 * @param float $y Y position 3826 * @param float $w Width 3827 * @param float $h Height 3828 * @param resource $img A GD resource 3829 * @param bool $is_mask true if the image is a mask 3830 * @param bool $mask true if the image is masked 3831 */ 3832 function addImagePng($file, $x, $y, $w = 0.0, $h = 0.0, &$img, $is_mask = false, $mask = null) { 3833 if (!function_exists("imagepng")) { 3834 throw new Exception("The PHP GD extension is required, but is not installed."); 3835 } 3836 3837 //if already cached, need not to read again 3838 if ( isset($this->imagelist[$file]) ) { 3839 $data = null; 3840 } 3841 else { 3842 // Example for transparency handling on new image. Retain for current image 3843 // $tIndex = imagecolortransparent($img); 3844 // if ($tIndex > 0) { 3845 // $tColor = imagecolorsforindex($img, $tIndex); 3846 // $new_tIndex = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']); 3847 // imagefill($new_img, 0, 0, $new_tIndex); 3848 // imagecolortransparent($new_img, $new_tIndex); 3849 // } 3850 // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn 3851 //imagealphablending($img, true); 3852 3853 //default, but explicitely set to ensure pdf compatibility 3854 imagesavealpha($img, false/*!$is_mask && !$mask*/); 3855 3856 $error = 0; 3857 //DEBUG_IMG_TEMP 3858 //debugpng 3859 if (DEBUGPNG) print '[addImagePng '.$file.']'; 3860 3861 ob_start(); 3862 @imagepng($img); 3863 $data = ob_get_clean(); 3864 3865 if ($data == '') { 3866 $error = 1; 3867 $errormsg = 'trouble writing file from GD'; 3868 //DEBUG_IMG_TEMP 3869 //debugpng 3870 if (DEBUGPNG) print 'trouble writing file from GD'; 3871 } 3872 3873 if ($error) { 3874 $this->addMessage('PNG error - ('.$file.') '.$errormsg); 3875 return; 3876 } 3877 } //End isset($this->imagelist[$file]) (png Duplicate removal) 3878 3879 $this->addPngFromBuf($file, $x, $y, $w, $h, $data, $is_mask, $mask); 3880 } 3881 3882 protected function addImagePngAlpha($file, $x, $y, $w, $h, $byte) { 3883 // generate images 3884 $img = imagecreatefrompng($file); 3885 3886 if ($img === false) { 3887 return; 3888 } 3889 3890 // FIXME The pixel transformation doesn't work well with 8bit PNGs 3891 $eight_bit = ($byte & 4) !== 4; 3892 3893 $wpx = imagesx($img); 3894 $hpx = imagesy($img); 3895 3896 imagesavealpha($img, false); 3897 3898 // create temp alpha file 3899 $tempfile_alpha = tempnam($this->tmp, "cpdf_img_"); 3900 @unlink($tempfile_alpha); 3901 $tempfile_alpha = "$tempfile_alpha.png"; 3902 3903 // create temp plain file 3904 $tempfile_plain = tempnam($this->tmp, "cpdf_img_"); 3905 @unlink($tempfile_plain); 3906 $tempfile_plain = "$tempfile_plain.png"; 3907 3908 $imgalpha = imagecreate($wpx, $hpx); 3909 imagesavealpha($imgalpha, false); 3910 3911 // generate gray scale palette (0 -> 255) 3912 for ($c = 0; $c < 256; ++$c) { 3913 imagecolorallocate($imgalpha, $c, $c, $c); 3914 } 3915 3916 // Use PECL gmagick + Graphics Magic to process transparent PNG images 3917 if (extension_loaded("gmagick")) { 3918 $gmagick = new Gmagick($file); 3919 $gmagick->setimageformat('png'); 3920 3921 // Get opacity channel (negative of alpha channel) 3922 $alpha_channel_neg = clone $gmagick; 3923 $alpha_channel_neg->separateimagechannel(Gmagick::CHANNEL_OPACITY); 3924 3925 // Negate opacity channel 3926 $alpha_channel = new Gmagick(); 3927 $alpha_channel->newimage($wpx, $hpx, "#FFFFFF", "png"); 3928 $alpha_channel->compositeimage($alpha_channel_neg, Gmagick::COMPOSITE_DIFFERENCE, 0, 0); 3929 $alpha_channel->separateimagechannel(Gmagick::CHANNEL_RED); 3930 $alpha_channel->writeimage($tempfile_alpha); 3931 3932 // Cast to 8bit+palette 3933 $imgalpha_ = imagecreatefrompng($tempfile_alpha); 3934 imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); 3935 imagedestroy($imgalpha_); 3936 imagepng($imgalpha, $tempfile_alpha); 3937 3938 // Make opaque image 3939 $color_channels = new Gmagick(); 3940 $color_channels->newimage($wpx, $hpx, "#FFFFFF", "png"); 3941 $color_channels->compositeimage($gmagick, Gmagick::COMPOSITE_COPYRED, 0, 0); 3942 $color_channels->compositeimage($gmagick, Gmagick::COMPOSITE_COPYGREEN, 0, 0); 3943 $color_channels->compositeimage($gmagick, Gmagick::COMPOSITE_COPYBLUE, 0, 0); 3944 $color_channels->writeimage($tempfile_plain); 3945 3946 $imgplain = imagecreatefrompng($tempfile_plain); 3947 } 3948 3949 // Use PECL imagick + ImageMagic to process transparent PNG images 3950 elseif (extension_loaded("imagick")) { 3951 // Native cloning was added to pecl-imagick in svn commit 263814 3952 // the first version containing it was 3.0.1RC1 3953 static $imagickClonable = null; 3954 if($imagickClonable === null) { 3955 $imagickClonable = version_compare(phpversion('imagick'), '3.0.1rc1') > 0; 3956 } 3957 3958 $imagick = new Imagick($file); 3959 $imagick->setFormat('png'); 3960 3961 // Get opacity channel (negative of alpha channel) 3962 $alpha_channel = $imagickClonable ? clone $imagick : $imagick->clone(); 3963 $alpha_channel->separateImageChannel(Imagick::CHANNEL_ALPHA); 3964 $alpha_channel->negateImage(true); 3965 $alpha_channel->writeImage($tempfile_alpha); 3966 3967 // Cast to 8bit+palette 3968 $imgalpha_ = imagecreatefrompng($tempfile_alpha); 3969 imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); 3970 imagedestroy($imgalpha_); 3971 imagepng($imgalpha, $tempfile_alpha); 3972 3973 // Make opaque image 3974 $color_channels = new Imagick(); 3975 $color_channels->newImage($wpx, $hpx, "#FFFFFF", "png"); 3976 $color_channels->compositeImage($imagick, Imagick::COMPOSITE_COPYRED, 0, 0); 3977 $color_channels->compositeImage($imagick, Imagick::COMPOSITE_COPYGREEN, 0, 0); 3978 $color_channels->compositeImage($imagick, Imagick::COMPOSITE_COPYBLUE, 0, 0); 3979 $color_channels->writeImage($tempfile_plain); 3980 3981 $imgplain = imagecreatefrompng($tempfile_plain); 3982 } 3983 else { 3984 // allocated colors cache 3985 $allocated_colors = array(); 3986 3987 // extract alpha channel 3988 for ($xpx = 0; $xpx < $wpx; ++$xpx) { 3989 for ($ypx = 0; $ypx < $hpx; ++$ypx) { 3990 $color = imagecolorat($img, $xpx, $ypx); 3991 $col = imagecolorsforindex($img, $color); 3992 $alpha = $col['alpha']; 3993 3994 if ($eight_bit) { 3995 // with gamma correction 3996 $gammacorr = 2.2; 3997 $pixel = pow((((127 - $alpha) * 255 / 127) / 255), $gammacorr) * 255; 3998 } 3999 4000 else { 4001 // without gamma correction 4002 $pixel = (127 - $alpha) * 2; 4003 4004 $key = $col['red'].$col['green'].$col['blue']; 4005 4006 if (!isset($allocated_colors[$key])) { 4007 $pixel_img = imagecolorallocate($img, $col['red'], $col['green'], $col['blue']); 4008 $allocated_colors[$key] = $pixel_img; 4009 } 4010 else { 4011 $pixel_img = $allocated_colors[$key]; 4012 } 4013 4014 imagesetpixel($img, $xpx, $ypx, $pixel_img); 4015 } 4016 4017 imagesetpixel($imgalpha, $xpx, $ypx, $pixel); 4018 } 4019 } 4020 4021 // extract image without alpha channel 4022 $imgplain = imagecreatetruecolor($wpx, $hpx); 4023 imagecopy($imgplain, $img, 0, 0, 0, 0, $wpx, $hpx); 4024 imagedestroy($img); 4025 4026 imagepng($imgalpha, $tempfile_alpha); 4027 imagepng($imgplain, $tempfile_plain); 4028 } 4029 4030 // embed mask image 4031 $this->addImagePng($tempfile_alpha, $x, $y, $w, $h, $imgalpha, true); 4032 imagedestroy($imgalpha); 4033 4034 // embed image, masked with previously embedded mask 4035 $this->addImagePng($tempfile_plain, $x, $y, $w, $h, $imgplain, false, true); 4036 imagedestroy($imgplain); 4037 4038 // remove temp files 4039 unlink($tempfile_alpha); 4040 unlink($tempfile_plain); 4041 } 4042 4043 /** 4044 * add a PNG image into the document, from a file 4045 * this should work with remote files 4046 */ 4047 function addPngFromFile($file, $x, $y, $w = 0, $h = 0) { 4048 if (!function_exists("imagecreatefrompng")) { 4049 throw new Exception("The PHP GD extension is required, but is not installed."); 4050 } 4051 4052 //if already cached, need not to read again 4053 if ( isset($this->imagelist[$file]) ) { 4054 $img = null; 4055 } 4056 4057 else { 4058 $info = file_get_contents ($file, false, null, 24, 5); 4059 $meta = unpack("CbitDepth/CcolorType/CcompressionMethod/CfilterMethod/CinterlaceMethod", $info); 4060 $bit_depth = $meta["bitDepth"]; 4061 $color_type = $meta["colorType"]; 4062 4063 // http://www.w3.org/TR/PNG/#11IHDR 4064 // 3 => indexed 4065 // 4 => greyscale with alpha 4066 // 6 => fullcolor with alpha 4067 $is_alpha = in_array($color_type, array(4, 6)) || ($color_type == 3 && $bit_depth != 4); 4068 4069 if ($is_alpha) { // exclude grayscale alpha 4070 return $this->addImagePngAlpha($file, $x, $y, $w, $h, $color_type); 4071 } 4072 4073 //png files typically contain an alpha channel. 4074 //pdf file format or class.pdf does not support alpha blending. 4075 //on alpha blended images, more transparent areas have a color near black. 4076 //This appears in the result on not storing the alpha channel. 4077 //Correct would be the box background image or its parent when transparent. 4078 //But this would make the image dependent on the background. 4079 //Therefore create an image with white background and copy in 4080 //A more natural background than black is white. 4081 //Therefore create an empty image with white background and merge the 4082 //image in with alpha blending. 4083 $imgtmp = @imagecreatefrompng($file); 4084 if (!$imgtmp) { 4085 return; 4086 } 4087 $sx = imagesx($imgtmp); 4088 $sy = imagesy($imgtmp); 4089 $img = imagecreatetruecolor($sx,$sy); 4090 imagealphablending($img, true); 4091 4092 // @todo is it still needed ?? 4093 $ti = imagecolortransparent($imgtmp); 4094 if ($ti >= 0) { 4095 $tc = imagecolorsforindex($imgtmp,$ti); 4096 $ti = imagecolorallocate($img,$tc['red'],$tc['green'],$tc['blue']); 4097 imagefill($img,0,0,$ti); 4098 imagecolortransparent($img, $ti); 4099 } else { 4100 imagefill($img,1,1,imagecolorallocate($img,255,255,255)); 4101 } 4102 4103 imagecopy($img,$imgtmp,0,0,0,0,$sx,$sy); 4104 imagedestroy($imgtmp); 4105 } 4106 $this->addImagePng($file, $x, $y, $w, $h, $img); 4107 4108 if ( $img ) { 4109 imagedestroy($img); 4110 } 4111 } 4112 4113 /** 4114 * add a PNG image into the document, from a memory buffer of the file 4115 */ 4116 function addPngFromBuf($file, $x, $y, $w = 0.0, $h = 0.0, &$data, $is_mask = false, $mask = null) { 4117 if ( isset($this->imagelist[$file]) ) { 4118 $data = null; 4119 $info['width'] = $this->imagelist[$file]['w']; 4120 $info['height'] = $this->imagelist[$file]['h']; 4121 $label = $this->imagelist[$file]['label']; 4122 } 4123 4124 else { 4125 if ($data == null) { 4126 $this->addMessage('addPngFromBuf error - data not present!'); 4127 return; 4128 } 4129 4130 $error = 0; 4131 4132 if (!$error) { 4133 $header = chr(137) .chr(80) .chr(78) .chr(71) .chr(13) .chr(10) .chr(26) .chr(10); 4134 4135 if (mb_substr($data, 0, 8, '8bit') != $header) { 4136 $error = 1; 4137 4138 if (DEBUGPNG) print '[addPngFromFile this file does not have a valid header '.$file.']'; 4139 4140 $errormsg = 'this file does not have a valid header'; 4141 } 4142 } 4143 4144 if (!$error) { 4145 // set pointer 4146 $p = 8; 4147 $len = mb_strlen($data, '8bit'); 4148 4149 // cycle through the file, identifying chunks 4150 $haveHeader = 0; 4151 $info = array(); 4152 $idata = ''; 4153 $pdata = ''; 4154 4155 while ($p < $len) { 4156 $chunkLen = $this->getBytes($data, $p, 4); 4157 $chunkType = mb_substr($data, $p+4, 4, '8bit'); 4158 4159 switch ($chunkType) { 4160 case 'IHDR': 4161 // this is where all the file information comes from 4162 $info['width'] = $this->getBytes($data, $p+8, 4); 4163 $info['height'] = $this->getBytes($data, $p+12, 4); 4164 $info['bitDepth'] = ord($data[$p+16]); 4165 $info['colorType'] = ord($data[$p+17]); 4166 $info['compressionMethod'] = ord($data[$p+18]); 4167 $info['filterMethod'] = ord($data[$p+19]); 4168 $info['interlaceMethod'] = ord($data[$p+20]); 4169 4170 //print_r($info); 4171 $haveHeader = 1; 4172 if ($info['compressionMethod'] != 0) { 4173 $error = 1; 4174 4175 //debugpng 4176 if (DEBUGPNG) print '[addPngFromFile unsupported compression method '.$file.']'; 4177 4178 $errormsg = 'unsupported compression method'; 4179 } 4180 4181 if ($info['filterMethod'] != 0) { 4182 $error = 1; 4183 4184 //debugpng 4185 if (DEBUGPNG) print '[addPngFromFile unsupported filter method '.$file.']'; 4186 4187 $errormsg = 'unsupported filter method'; 4188 } 4189 break; 4190 4191 case 'PLTE': 4192 $pdata.= mb_substr($data, $p+8, $chunkLen, '8bit'); 4193 break; 4194 4195 case 'IDAT': 4196 $idata.= mb_substr($data, $p+8, $chunkLen, '8bit'); 4197 break; 4198 4199 case 'tRNS': 4200 //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk 4201 //print "tRNS found, color type = ".$info['colorType']."\n"; 4202 $transparency = array(); 4203 4204 switch ($info['colorType']) { 4205 // indexed color, rbg 4206 case 3: 4207 /* corresponding to entries in the plte chunk 4208 Alpha for palette index 0: 1 byte 4209 Alpha for palette index 1: 1 byte 4210 ...etc... 4211 */ 4212 // there will be one entry for each palette entry. up until the last non-opaque entry. 4213 // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent) 4214 $transparency['type'] = 'indexed'; 4215 $trans = 0; 4216 4217 for ($i = $chunkLen; $i >= 0; $i--) { 4218 if (ord($data[$p+8+$i]) == 0) { 4219 $trans = $i; 4220 } 4221 } 4222 4223 $transparency['data'] = $trans; 4224 break; 4225 4226 // grayscale 4227 case 0: 4228 /* corresponding to entries in the plte chunk 4229 Gray: 2 bytes, range 0 .. (2^bitdepth)-1 4230 */ 4231 // $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale 4232 $transparency['type'] = 'indexed'; 4233 $transparency['data'] = ord($data[$p+8+1]); 4234 break; 4235 4236 // truecolor 4237 case 2: 4238 /* corresponding to entries in the plte chunk 4239 Red: 2 bytes, range 0 .. (2^bitdepth)-1 4240 Green: 2 bytes, range 0 .. (2^bitdepth)-1 4241 Blue: 2 bytes, range 0 .. (2^bitdepth)-1 4242 */ 4243 $transparency['r'] = $this->getBytes($data, $p+8, 2); 4244 // r from truecolor 4245 $transparency['g'] = $this->getBytes($data, $p+10, 2); 4246 // g from truecolor 4247 $transparency['b'] = $this->getBytes($data, $p+12, 2); 4248 // b from truecolor 4249 4250 $transparency['type'] = 'color-key'; 4251 break; 4252 4253 //unsupported transparency type 4254 default: 4255 if (DEBUGPNG) print '[addPngFromFile unsupported transparency type '.$file.']'; 4256 break; 4257 } 4258 4259 // KS End new code 4260 break; 4261 4262 default: 4263 break; 4264 } 4265 4266 $p += $chunkLen+12; 4267 } 4268 4269 if (!$haveHeader) { 4270 $error = 1; 4271 4272 //debugpng 4273 if (DEBUGPNG) print '[addPngFromFile information header is missing '.$file.']'; 4274 4275 $errormsg = 'information header is missing'; 4276 } 4277 4278 if (isset($info['interlaceMethod']) && $info['interlaceMethod']) { 4279 $error = 1; 4280 4281 //debugpng 4282 if (DEBUGPNG) print '[addPngFromFile no support for interlaced images in pdf '.$file.']'; 4283 4284 $errormsg = 'There appears to be no support for interlaced images in pdf.'; 4285 } 4286 } 4287 4288 if (!$error && $info['bitDepth'] > 8) { 4289 $error = 1; 4290 4291 //debugpng 4292 if (DEBUGPNG) print '[addPngFromFile bit depth of 8 or less is supported '.$file.']'; 4293 4294 $errormsg = 'only bit depth of 8 or less is supported'; 4295 } 4296 4297 if (!$error) { 4298 switch ($info['colorType']) { 4299 case 3: 4300 $color = 'DeviceRGB'; 4301 $ncolor = 1; 4302 break; 4303 4304 case 2: 4305 $color = 'DeviceRGB'; 4306 $ncolor = 3; 4307 break; 4308 4309 case 0: 4310 $color = 'DeviceGray'; 4311 $ncolor = 1; 4312 break; 4313 4314 default: 4315 $error = 1; 4316 4317 //debugpng 4318 if (DEBUGPNG) print '[addPngFromFile alpha channel not supported: '.$info['colorType'].' '.$file.']'; 4319 4320 $errormsg = 'transparancey alpha channel not supported, transparency only supported for palette images.'; 4321 } 4322 } 4323 4324 if ($error) { 4325 $this->addMessage('PNG error - ('.$file.') '.$errormsg); 4326 return; 4327 } 4328 4329 //print_r($info); 4330 // so this image is ok... add it in. 4331 $this->numImages++; 4332 $im = $this->numImages; 4333 $label = "I$im"; 4334 $this->numObj++; 4335 4336 // $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width'])); 4337 $options = array( 4338 'label' => $label, 4339 'data' => $idata, 4340 'bitsPerComponent' => $info['bitDepth'], 4341 'pdata' => $pdata, 4342 'iw' => $info['width'], 4343 'ih' => $info['height'], 4344 'type' => 'png', 4345 'color' => $color, 4346 'ncolor' => $ncolor, 4347 'masked' => $mask, 4348 'isMask' => $is_mask 4349 ); 4350 4351 if (isset($transparency)) { 4352 $options['transparency'] = $transparency; 4353 } 4354 4355 $this->o_image($this->numObj, 'new', $options); 4356 $this->imagelist[$file] = array('label' =>$label, 'w' => $info['width'], 'h' => $info['height']); 4357 } 4358 4359 if ($is_mask) { 4360 return; 4361 } 4362 4363 if ($w <= 0 && $h <= 0) { 4364 $w = $info['width']; 4365 $h = $info['height']; 4366 } 4367 4368 if ($w <= 0) { 4369 $w = $h/$info['height']*$info['width']; 4370 } 4371 4372 if ($h <= 0) { 4373 $h = $w*$info['height']/$info['width']; 4374 } 4375 4376 $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ", $w, $h, $x, $y, $label)); 4377 } 4378 4379 /** 4380 * add a JPEG image into the document, from a file 4381 */ 4382 function addJpegFromFile($img, $x, $y, $w = 0, $h = 0) { 4383 // attempt to add a jpeg image straight from a file, using no GD commands 4384 // note that this function is unable to operate on a remote file. 4385 4386 if (!file_exists($img)) { 4387 return; 4388 } 4389 4390 if ( $this->image_iscached($img) ) { 4391 $data = null; 4392 $imageWidth = $this->imagelist[$img]['w']; 4393 $imageHeight = $this->imagelist[$img]['h']; 4394 $channels = $this->imagelist[$img]['c']; 4395 } 4396 else { 4397 $tmp = getimagesize($img); 4398 $imageWidth = $tmp[0]; 4399 $imageHeight = $tmp[1]; 4400 4401 if ( isset($tmp['channels']) ) { 4402 $channels = $tmp['channels']; 4403 } else { 4404 $channels = 3; 4405 } 4406 4407 $data = file_get_contents($img); 4408 } 4409 4410 if ($w <= 0 && $h <= 0) { 4411 $w = $imageWidth; 4412 } 4413 4414 if ($w == 0) { 4415 $w = $h/$imageHeight*$imageWidth; 4416 } 4417 4418 if ($h == 0) { 4419 $h = $w*$imageHeight/$imageWidth; 4420 } 4421 4422 $this->addJpegImage_common($data, $x, $y, $w, $h, $imageWidth, $imageHeight, $channels, $img); 4423 } 4424 4425 /** 4426 * common code used by the two JPEG adding functions 4427 */ 4428 private function addJpegImage_common(&$data, $x, $y, $w = 0, $h = 0, $imageWidth, $imageHeight, $channels = 3, $imgname) { 4429 if ( $this->image_iscached($imgname) ) { 4430 $label = $this->imagelist[$imgname]['label']; 4431 //debugpng 4432 //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']'; 4433 4434 } else { 4435 if ($data == null) { 4436 $this->addMessage('addJpegImage_common error - ('.$imgname.') data not present!'); 4437 return; 4438 } 4439 4440 // note that this function is not to be called externally 4441 // it is just the common code between the GD and the file options 4442 $this->numImages++; 4443 $im = $this->numImages; 4444 $label = "I$im"; 4445 $this->numObj++; 4446 4447 $this->o_image($this->numObj, 'new', array( 4448 'label' => $label, 4449 'data' => &$data, 4450 'iw' => $imageWidth, 4451 'ih' => $imageHeight, 4452 'channels' => $channels 4453 )); 4454 4455 $this->imagelist[$imgname] = array('label' =>$label, 'w' => $imageWidth, 'h' => $imageHeight, 'c'=> $channels); 4456 } 4457 4458 $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ ", $w, $h, $x, $y, $label)); 4459 } 4460 4461 /** 4462 * specify where the document should open when it first starts 4463 */ 4464 function openHere($style, $a = 0, $b = 0, $c = 0) { 4465 // this function will open the document at a specified page, in a specified style 4466 // the values for style, and the required paramters are: 4467 // 'XYZ' left, top, zoom 4468 // 'Fit' 4469 // 'FitH' top 4470 // 'FitV' left 4471 // 'FitR' left,bottom,right 4472 // 'FitB' 4473 // 'FitBH' top 4474 // 'FitBV' left 4475 $this->numObj++; 4476 $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c)); 4477 $id = $this->catalogId; 4478 $this->o_catalog($id, 'openHere', $this->numObj); 4479 } 4480 4481 /** 4482 * Add JavaScript code to the PDF document 4483 * 4484 * @param string $code 4485 * @return void 4486 */ 4487 function addJavascript($code) { 4488 $this->javascript .= $code; 4489 } 4490 4491 /** 4492 * create a labelled destination within the document 4493 */ 4494 function addDestination($label, $style, $a = 0, $b = 0, $c = 0) { 4495 // associates the given label with the destination, it is done this way so that a destination can be specified after 4496 // it has been linked to 4497 // styles are the same as the 'openHere' function 4498 $this->numObj++; 4499 $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c)); 4500 $id = $this->numObj; 4501 4502 // store the label->idf relationship, note that this means that labels can be used only once 4503 $this->destinations["$label"] = $id; 4504 } 4505 4506 /** 4507 * define font families, this is used to initialize the font families for the default fonts 4508 * and for the user to add new ones for their fonts. The default bahavious can be overridden should 4509 * that be desired. 4510 */ 4511 function setFontFamily($family, $options = '') { 4512 if (!is_array($options)) { 4513 if ($family === 'init') { 4514 // set the known family groups 4515 // these font families will be used to enable bold and italic markers to be included 4516 // within text streams. html forms will be used... <b></b> <i></i> 4517 $this->fontFamilies['Helvetica.afm'] = 4518 array( 4519 'b' => 'Helvetica-Bold.afm', 4520 'i' => 'Helvetica-Oblique.afm', 4521 'bi' => 'Helvetica-BoldOblique.afm', 4522 'ib' => 'Helvetica-BoldOblique.afm' 4523 ); 4524 4525 $this->fontFamilies['Courier.afm'] = 4526 array( 4527 'b' => 'Courier-Bold.afm', 4528 'i' => 'Courier-Oblique.afm', 4529 'bi' => 'Courier-BoldOblique.afm', 4530 'ib' => 'Courier-BoldOblique.afm' 4531 ); 4532 4533 $this->fontFamilies['Times-Roman.afm'] = 4534 array( 4535 'b' => 'Times-Bold.afm', 4536 'i' => 'Times-Italic.afm', 4537 'bi' => 'Times-BoldItalic.afm', 4538 'ib' => 'Times-BoldItalic.afm' 4539 ); 4540 } 4541 } else { 4542 4543 // the user is trying to set a font family 4544 // note that this can also be used to set the base ones to something else 4545 if (mb_strlen($family)) { 4546 $this->fontFamilies[$family] = $options; 4547 } 4548 } 4549 } 4550 4551 /** 4552 * used to add messages for use in debugging 4553 */ 4554 function addMessage($message) { 4555 $this->messages.= $message."\n"; 4556 } 4557 4558 /** 4559 * a few functions which should allow the document to be treated transactionally. 4560 */ 4561 function transaction($action) { 4562 switch ($action) { 4563 case 'start': 4564 // store all the data away into the checkpoint variable 4565 $data = get_object_vars($this); 4566 $this->checkpoint = $data; 4567 unset($data); 4568 break; 4569 4570 case 'commit': 4571 if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])) { 4572 $tmp = $this->checkpoint['checkpoint']; 4573 $this->checkpoint = $tmp; 4574 unset($tmp); 4575 } else { 4576 $this->checkpoint = ''; 4577 } 4578 break; 4579 4580 case 'rewind': 4581 // do not destroy the current checkpoint, but move us back to the state then, so that we can try again 4582 if (is_array($this->checkpoint)) { 4583 // can only abort if were inside a checkpoint 4584 $tmp = $this->checkpoint; 4585 4586 foreach ($tmp as $k => $v) { 4587 if ($k !== 'checkpoint') { 4588 $this->$k = $v; 4589 } 4590 } 4591 unset($tmp); 4592 } 4593 break; 4594 4595 case 'abort': 4596 if (is_array($this->checkpoint)) { 4597 // can only abort if were inside a checkpoint 4598 $tmp = $this->checkpoint; 4599 foreach ($tmp as $k => $v) { 4600 $this->$k = $v; 4601 } 4602 unset($tmp); 4603 } 4604 break; 4605 } 4606 } 4607}