/framework/vendor/zend/Zend/Pdf.php
PHP | 1404 lines | 748 code | 189 blank | 467 comment | 166 complexity | b8fbed63f678ae1bc1985a5b57eb9380 MD5 | raw file
1<?php 2/** 3 * Zend Framework 4 * 5 * LICENSE 6 * 7 * This source file is subject to the new BSD license that is bundled 8 * with this package in the file LICENSE.txt. 9 * It is also available through the world-wide-web at this URL: 10 * http://framework.zend.com/license/new-bsd 11 * If you did not receive a copy of the license and are unable to 12 * obtain it through the world-wide-web, please send an email 13 * to license@zend.com so we can send you a copy immediately. 14 * 15 * @category Zend 16 * @package Zend_Pdf 17 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) 18 * @license http://framework.zend.com/license/new-bsd New BSD License 19 * @version $Id: Pdf.php 20096 2010-01-06 02:05:09Z bkarwin $ 20 */ 21 22 23/** User land classes and interfaces turned on by Zend/Pdf.php file inclusion. */ 24/** @todo Section should be removed with ZF 2.0 release as obsolete */ 25 26/** Zend_Pdf_Page */ 27require_once 'Zend/Pdf/Page.php'; 28 29/** Zend_Pdf_Style */ 30require_once 'Zend/Pdf/Style.php'; 31 32/** Zend_Pdf_Color_GrayScale */ 33require_once 'Zend/Pdf/Color/GrayScale.php'; 34 35/** Zend_Pdf_Color_Rgb */ 36require_once 'Zend/Pdf/Color/Rgb.php'; 37 38/** Zend_Pdf_Color_Cmyk */ 39require_once 'Zend/Pdf/Color/Cmyk.php'; 40 41/** Zend_Pdf_Color_Html */ 42require_once 'Zend/Pdf/Color/Html.php'; 43 44/** Zend_Pdf_Image */ 45require_once 'Zend/Pdf/Image.php'; 46 47/** Zend_Pdf_Font */ 48require_once 'Zend/Pdf/Font.php'; 49 50 51/** Internally used classes */ 52require_once 'Zend/Pdf/Element.php'; 53require_once 'Zend/Pdf/Element/Array.php'; 54require_once 'Zend/Pdf/Element/String/Binary.php'; 55require_once 'Zend/Pdf/Element/Boolean.php'; 56require_once 'Zend/Pdf/Element/Dictionary.php'; 57require_once 'Zend/Pdf/Element/Name.php'; 58require_once 'Zend/Pdf/Element/Null.php'; 59require_once 'Zend/Pdf/Element/Numeric.php'; 60require_once 'Zend/Pdf/Element/String.php'; 61 62 63/** 64 * General entity which describes PDF document. 65 * It implements document abstraction with a document level operations. 66 * 67 * Class is used to create new PDF document or load existing document. 68 * See details in a class constructor description 69 * 70 * Class agregates document level properties and entities (pages, bookmarks, 71 * document level actions, attachments, form object, etc) 72 * 73 * @category Zend 74 * @package Zend_Pdf 75 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) 76 * @license http://framework.zend.com/license/new-bsd New BSD License 77 */ 78class Zend_Pdf 79{ 80 /**** Class Constants ****/ 81 82 /** 83 * Version number of generated PDF documents. 84 */ 85 const PDF_VERSION = '1.4'; 86 87 /** 88 * PDF file header. 89 */ 90 const PDF_HEADER = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"; 91 92 93 94 /** 95 * Pages collection 96 * 97 * @todo implement it as a class, which supports ArrayAccess and Iterator interfaces, 98 * to provide incremental parsing and pages tree updating. 99 * That will give good performance and memory (PDF size) benefits. 100 * 101 * @var array - array of Zend_Pdf_Page object 102 */ 103 public $pages = array(); 104 105 /** 106 * Document properties 107 * 108 * It's an associative array with PDF meta information, values may 109 * be string, boolean or float. 110 * Returned array could be used directly to access, add, modify or remove 111 * document properties. 112 * 113 * Standard document properties: Title (must be set for PDF/X documents), Author, 114 * Subject, Keywords (comma separated list), Creator (the name of the application, 115 * that created document, if it was converted from other format), Trapped (must be 116 * true, false or null, can not be null for PDF/X documents) 117 * 118 * @var array 119 */ 120 public $properties = array(); 121 122 /** 123 * Original properties set. 124 * 125 * Used for tracking properties changes 126 * 127 * @var array 128 */ 129 protected $_originalProperties = array(); 130 131 /** 132 * Document level javascript 133 * 134 * @var string 135 */ 136 protected $_javaScript = null; 137 138 /** 139 * Document named destinations or "GoTo..." actions, used to refer 140 * document parts from outside PDF 141 * 142 * @var array - array of Zend_Pdf_Target objects 143 */ 144 protected $_namedTargets = array(); 145 146 /** 147 * Document outlines 148 * 149 * @var array - array of Zend_Pdf_Outline objects 150 */ 151 public $outlines = array(); 152 153 /** 154 * Original document outlines list 155 * Used to track outlines update 156 * 157 * @var array - array of Zend_Pdf_Outline objects 158 */ 159 protected $_originalOutlines = array(); 160 161 /** 162 * Original document outlines open elements count 163 * Used to track outlines update 164 * 165 * @var integer 166 */ 167 protected $_originalOpenOutlinesCount = 0; 168 169 /** 170 * Pdf trailer (last or just created) 171 * 172 * @var Zend_Pdf_Trailer 173 */ 174 protected $_trailer = null; 175 176 /** 177 * PDF objects factory. 178 * 179 * @var Zend_Pdf_ElementFactory_Interface 180 */ 181 protected $_objFactory = null; 182 183 /** 184 * Memory manager for stream objects 185 * 186 * @var Zend_Memory_Manager|null 187 */ 188 protected static $_memoryManager = null; 189 190 /** 191 * Pdf file parser. 192 * It's not used, but has to be destroyed only with Zend_Pdf object 193 * 194 * @var Zend_Pdf_Parser 195 */ 196 protected $_parser; 197 198 199 /** 200 * List of inheritable attributesfor pages tree 201 * 202 * @var array 203 */ 204 protected static $_inheritableAttributes = array('Resources', 'MediaBox', 'CropBox', 'Rotate'); 205 206 /** 207 * Request used memory manager 208 * 209 * @return Zend_Memory_Manager 210 */ 211 static public function getMemoryManager() 212 { 213 if (self::$_memoryManager === null) { 214 require_once 'Zend/Memory.php'; 215 self::$_memoryManager = Zend_Memory::factory('none'); 216 } 217 218 return self::$_memoryManager; 219 } 220 221 /** 222 * Set user defined memory manager 223 * 224 * @param Zend_Memory_Manager $memoryManager 225 */ 226 static public function setMemoryManager(Zend_Memory_Manager $memoryManager) 227 { 228 self::$_memoryManager = $memoryManager; 229 } 230 231 232 /** 233 * Create new PDF document from a $source string 234 * 235 * @param string $source 236 * @param integer $revision 237 * @return Zend_Pdf 238 */ 239 public static function parse(&$source = null, $revision = null) 240 { 241 return new Zend_Pdf($source, $revision); 242 } 243 244 /** 245 * Load PDF document from a file 246 * 247 * @param string $source 248 * @param integer $revision 249 * @return Zend_Pdf 250 */ 251 public static function load($source = null, $revision = null) 252 { 253 return new Zend_Pdf($source, $revision, true); 254 } 255 256 /** 257 * Render PDF document and save it. 258 * 259 * If $updateOnly is true, then it only appends new section to the end of file. 260 * 261 * @param string $filename 262 * @param boolean $updateOnly 263 * @throws Zend_Pdf_Exception 264 */ 265 public function save($filename, $updateOnly = false) 266 { 267 if (($file = @fopen($filename, $updateOnly ? 'ab':'wb')) === false ) { 268 require_once 'Zend/Pdf/Exception.php'; 269 throw new Zend_Pdf_Exception( "Can not open '$filename' file for writing." ); 270 } 271 272 $this->render($updateOnly, $file); 273 274 fclose($file); 275 } 276 277 /** 278 * Creates or loads PDF document. 279 * 280 * If $source is null, then it creates a new document. 281 * 282 * If $source is a string and $load is false, then it loads document 283 * from a binary string. 284 * 285 * If $source is a string and $load is true, then it loads document 286 * from a file. 287 288 * $revision used to roll back document to specified version 289 * (0 - currtent version, 1 - previous version, 2 - ...) 290 * 291 * @param string $source - PDF file to load 292 * @param integer $revision 293 * @throws Zend_Pdf_Exception 294 * @return Zend_Pdf 295 */ 296 public function __construct($source = null, $revision = null, $load = false) 297 { 298 require_once 'Zend/Pdf/ElementFactory.php'; 299 $this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1); 300 301 if ($source !== null) { 302 require_once 'Zend/Pdf/Parser.php'; 303 $this->_parser = new Zend_Pdf_Parser($source, $this->_objFactory, $load); 304 $this->_pdfHeaderVersion = $this->_parser->getPDFVersion(); 305 $this->_trailer = $this->_parser->getTrailer(); 306 if ($this->_trailer->Encrypt !== null) { 307 require_once 'Zend/Pdf/Exception.php'; 308 throw new Zend_Pdf_Exception('Encrypted document modification is not supported'); 309 } 310 if ($revision !== null) { 311 $this->rollback($revision); 312 } else { 313 $this->_loadPages($this->_trailer->Root->Pages); 314 } 315 316 $this->_loadNamedDestinations($this->_trailer->Root, $this->_parser->getPDFVersion()); 317 $this->_loadOutlines($this->_trailer->Root); 318 319 if ($this->_trailer->Info !== null) { 320 $this->properties = $this->_trailer->Info->toPhp(); 321 322 if (isset($this->properties['Trapped'])) { 323 switch ($this->properties['Trapped']) { 324 case 'True': 325 $this->properties['Trapped'] = true; 326 break; 327 328 case 'False': 329 $this->properties['Trapped'] = false; 330 break; 331 332 case 'Unknown': 333 $this->properties['Trapped'] = null; 334 break; 335 336 default: 337 // Wrong property value 338 // Do nothing 339 break; 340 } 341 } 342 343 $this->_originalProperties = $this->properties; 344 } 345 } else { 346 $this->_pdfHeaderVersion = Zend_Pdf::PDF_VERSION; 347 348 $trailerDictionary = new Zend_Pdf_Element_Dictionary(); 349 350 /** 351 * Document id 352 */ 353 $docId = md5(uniqid(rand(), true)); // 32 byte (128 bit) identifier 354 $docIdLow = substr($docId, 0, 16); // first 16 bytes 355 $docIdHigh = substr($docId, 16, 16); // second 16 bytes 356 357 $trailerDictionary->ID = new Zend_Pdf_Element_Array(); 358 $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdLow); 359 $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdHigh); 360 361 $trailerDictionary->Size = new Zend_Pdf_Element_Numeric(0); 362 363 require_once 'Zend/Pdf/Trailer/Generator.php'; 364 $this->_trailer = new Zend_Pdf_Trailer_Generator($trailerDictionary); 365 366 /** 367 * Document catalog indirect object. 368 */ 369 $docCatalog = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 370 $docCatalog->Type = new Zend_Pdf_Element_Name('Catalog'); 371 $docCatalog->Version = new Zend_Pdf_Element_Name(Zend_Pdf::PDF_VERSION); 372 $this->_trailer->Root = $docCatalog; 373 374 /** 375 * Pages container 376 */ 377 $docPages = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 378 $docPages->Type = new Zend_Pdf_Element_Name('Pages'); 379 $docPages->Kids = new Zend_Pdf_Element_Array(); 380 $docPages->Count = new Zend_Pdf_Element_Numeric(0); 381 $docCatalog->Pages = $docPages; 382 } 383 } 384 385 /** 386 * Retrive number of revisions. 387 * 388 * @return integer 389 */ 390 public function revisions() 391 { 392 $revisions = 1; 393 $currentTrailer = $this->_trailer; 394 395 while ($currentTrailer->getPrev() !== null && $currentTrailer->getPrev()->Root !== null ) { 396 $revisions++; 397 $currentTrailer = $currentTrailer->getPrev(); 398 } 399 400 return $revisions++; 401 } 402 403 /** 404 * Rollback document $steps number of revisions. 405 * This method must be invoked before any changes, applied to the document. 406 * Otherwise behavior is undefined. 407 * 408 * @param integer $steps 409 */ 410 public function rollback($steps) 411 { 412 for ($count = 0; $count < $steps; $count++) { 413 if ($this->_trailer->getPrev() !== null && $this->_trailer->getPrev()->Root !== null) { 414 $this->_trailer = $this->_trailer->getPrev(); 415 } else { 416 break; 417 } 418 } 419 $this->_objFactory->setObjectCount($this->_trailer->Size->value); 420 421 // Mark content as modified to force new trailer generation at render time 422 $this->_trailer->Root->touch(); 423 424 $this->pages = array(); 425 $this->_loadPages($this->_trailer->Root->Pages); 426 } 427 428 429 /** 430 * Load pages recursively 431 * 432 * @param Zend_Pdf_Element_Reference $pages 433 * @param array|null $attributes 434 */ 435 protected function _loadPages(Zend_Pdf_Element_Reference $pages, $attributes = array()) 436 { 437 if ($pages->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) { 438 require_once 'Zend/Pdf/Exception.php'; 439 throw new Zend_Pdf_Exception('Wrong argument'); 440 } 441 442 foreach ($pages->getKeys() as $property) { 443 if (in_array($property, self::$_inheritableAttributes)) { 444 $attributes[$property] = $pages->$property; 445 $pages->$property = null; 446 } 447 } 448 449 450 foreach ($pages->Kids->items as $child) { 451 if ($child->Type->value == 'Pages') { 452 $this->_loadPages($child, $attributes); 453 } else if ($child->Type->value == 'Page') { 454 foreach (self::$_inheritableAttributes as $property) { 455 if ($child->$property === null && array_key_exists($property, $attributes)) { 456 /** 457 * Important note. 458 * If any attribute or dependant object is an indirect object, then it's still 459 * shared between pages. 460 */ 461 if ($attributes[$property] instanceof Zend_Pdf_Element_Object || 462 $attributes[$property] instanceof Zend_Pdf_Element_Reference) { 463 $child->$property = $attributes[$property]; 464 } else { 465 $child->$property = $this->_objFactory->newObject($attributes[$property]); 466 } 467 } 468 } 469 470 require_once 'Zend/Pdf/Page.php'; 471 $this->pages[] = new Zend_Pdf_Page($child, $this->_objFactory); 472 } 473 } 474 } 475 476 /** 477 * Load named destinations recursively 478 * 479 * @param Zend_Pdf_Element_Reference $root Document catalog entry 480 * @param string $pdfHeaderVersion 481 * @throws Zend_Pdf_Exception 482 */ 483 protected function _loadNamedDestinations(Zend_Pdf_Element_Reference $root, $pdfHeaderVersion) 484 { 485 if ($root->Version !== null && version_compare($root->Version->value, $pdfHeaderVersion, '>')) { 486 $versionIs_1_2_plus = version_compare($root->Version->value, '1.1', '>'); 487 } else { 488 $versionIs_1_2_plus = version_compare($pdfHeaderVersion, '1.1', '>'); 489 } 490 491 if ($versionIs_1_2_plus) { 492 // PDF version is 1.2+ 493 // Look for Destinations structure at Name dictionary 494 if ($root->Names !== null && $root->Names->Dests !== null) { 495 require_once 'Zend/Pdf/NameTree.php'; 496 require_once 'Zend/Pdf/Target.php'; 497 foreach (new Zend_Pdf_NameTree($root->Names->Dests) as $name => $destination) { 498 $this->_namedTargets[$name] = Zend_Pdf_Target::load($destination); 499 } 500 } 501 } else { 502 // PDF version is 1.1 (or earlier) 503 // Look for Destinations sructure at Dest entry of document catalog 504 if ($root->Dests !== null) { 505 if ($root->Dests->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) { 506 require_once 'Zend/Pdf/Exception.php'; 507 throw new Zend_Pdf_Exception('Document catalog Dests entry must be a dictionary.'); 508 } 509 510 require_once 'Zend/Pdf/Target.php'; 511 foreach ($root->Dests->getKeys() as $destKey) { 512 $this->_namedTargets[$destKey] = Zend_Pdf_Target::load($root->Dests->$destKey); 513 } 514 } 515 } 516 } 517 518 /** 519 * Load outlines recursively 520 * 521 * @param Zend_Pdf_Element_Reference $root Document catalog entry 522 */ 523 protected function _loadOutlines(Zend_Pdf_Element_Reference $root) 524 { 525 if ($root->Outlines === null) { 526 return; 527 } 528 529 if ($root->Outlines->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) { 530 require_once 'Zend/Pdf/Exception.php'; 531 throw new Zend_Pdf_Exception('Document catalog Outlines entry must be a dictionary.'); 532 } 533 534 if ($root->Outlines->Type !== null && $root->Outlines->Type->value != 'Outlines') { 535 require_once 'Zend/Pdf/Exception.php'; 536 throw new Zend_Pdf_Exception('Outlines Type entry must be an \'Outlines\' string.'); 537 } 538 539 if ($root->Outlines->First === null) { 540 return; 541 } 542 543 $outlineDictionary = $root->Outlines->First; 544 $processedDictionaries = new SplObjectStorage(); 545 while ($outlineDictionary !== null && !$processedDictionaries->contains($outlineDictionary)) { 546 $processedDictionaries->attach($outlineDictionary); 547 548 require_once 'Zend/Pdf/Outline/Loaded.php'; 549 $this->outlines[] = new Zend_Pdf_Outline_Loaded($outlineDictionary); 550 551 $outlineDictionary = $outlineDictionary->Next; 552 } 553 554 $this->_originalOutlines = $this->outlines; 555 556 if ($root->Outlines->Count !== null) { 557 $this->_originalOpenOutlinesCount = $root->Outlines->Count->value; 558 } 559 } 560 561 /** 562 * Orginize pages to tha pages tree structure. 563 * 564 * @todo atomatically attach page to the document, if it's not done yet. 565 * @todo check, that page is attached to the current document 566 * 567 * @todo Dump pages as a balanced tree instead of a plain set. 568 */ 569 protected function _dumpPages() 570 { 571 $root = $this->_trailer->Root; 572 $pagesContainer = $root->Pages; 573 574 $pagesContainer->touch(); 575 $pagesContainer->Kids->items = array(); 576 577 foreach ($this->pages as $page ) { 578 $page->render($this->_objFactory); 579 580 $pageDictionary = $page->getPageDictionary(); 581 $pageDictionary->touch(); 582 $pageDictionary->Parent = $pagesContainer; 583 584 $pagesContainer->Kids->items[] = $pageDictionary; 585 } 586 587 $this->_refreshPagesHash(); 588 589 $pagesContainer->Count->touch(); 590 $pagesContainer->Count->value = count($this->pages); 591 592 593 // Refresh named destinations list 594 foreach ($this->_namedTargets as $name => $namedTarget) { 595 if ($namedTarget instanceof Zend_Pdf_Destination_Explicit) { 596 // Named target is an explicit destination 597 if ($this->resolveDestination($namedTarget, false) === null) { 598 unset($this->_namedTargets[$name]); 599 } 600 } else if ($namedTarget instanceof Zend_Pdf_Action) { 601 // Named target is an action 602 if ($this->_cleanUpAction($namedTarget, false) === null) { 603 // Action is a GoTo action with an unresolved destination 604 unset($this->_namedTargets[$name]); 605 } 606 } else { 607 require_once 'Zend/Pdf/Exception.php'; 608 throw new Zend_Pdf_Exception('Wrong type of named targed (\'' . get_class($namedTarget) . '\').'); 609 } 610 } 611 612 // Refresh outlines 613 require_once 'Zend/Pdf/RecursivelyIteratableObjectsContainer.php'; 614 $iterator = new RecursiveIteratorIterator(new Zend_Pdf_RecursivelyIteratableObjectsContainer($this->outlines), RecursiveIteratorIterator::SELF_FIRST); 615 foreach ($iterator as $outline) { 616 $target = $outline->getTarget(); 617 618 if ($target !== null) { 619 if ($target instanceof Zend_Pdf_Destination) { 620 // Outline target is a destination 621 if ($this->resolveDestination($target, false) === null) { 622 $outline->setTarget(null); 623 } 624 } else if ($target instanceof Zend_Pdf_Action) { 625 // Outline target is an action 626 if ($this->_cleanUpAction($target, false) === null) { 627 // Action is a GoTo action with an unresolved destination 628 $outline->setTarget(null); 629 } 630 } else { 631 require_once 'Zend/Pdf/Exception.php'; 632 throw new Zend_Pdf_Exception('Wrong outline target.'); 633 } 634 } 635 } 636 637 $openAction = $this->getOpenAction(); 638 if ($openAction !== null) { 639 if ($openAction instanceof Zend_Pdf_Action) { 640 // OpenAction is an action 641 if ($this->_cleanUpAction($openAction, false) === null) { 642 // Action is a GoTo action with an unresolved destination 643 $this->setOpenAction(null); 644 } 645 } else if ($openAction instanceof Zend_Pdf_Destination) { 646 // OpenAction target is a destination 647 if ($this->resolveDestination($openAction, false) === null) { 648 $this->setOpenAction(null); 649 } 650 } else { 651 require_once 'Zend/Pdf/Exception.php'; 652 throw new Zend_Pdf_Exception('OpenAction has to be either PDF Action or Destination.'); 653 } 654 } 655 } 656 657 /** 658 * Dump named destinations 659 * 660 * @todo Create a balanced tree instead of plain structure. 661 */ 662 protected function _dumpNamedDestinations() 663 { 664 ksort($this->_namedTargets, SORT_STRING); 665 666 $destArrayItems = array(); 667 foreach ($this->_namedTargets as $name => $destination) { 668 $destArrayItems[] = new Zend_Pdf_Element_String($name); 669 670 if ($destination instanceof Zend_Pdf_Target) { 671 $destArrayItems[] = $destination->getResource(); 672 } else { 673 require_once 'Zend/Pdf/Exception.php'; 674 throw new Zend_Pdf_Exception('PDF named destinations must be a Zend_Pdf_Target object.'); 675 } 676 } 677 $destArray = $this->_objFactory->newObject(new Zend_Pdf_Element_Array($destArrayItems)); 678 679 $DestTree = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 680 $DestTree->Names = $destArray; 681 682 $root = $this->_trailer->Root; 683 684 if ($root->Names === null) { 685 $root->touch(); 686 $root->Names = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 687 } else { 688 $root->Names->touch(); 689 } 690 $root->Names->Dests = $DestTree; 691 } 692 693 /** 694 * Dump outlines recursively 695 */ 696 protected function _dumpOutlines() 697 { 698 $root = $this->_trailer->Root; 699 700 if ($root->Outlines === null) { 701 if (count($this->outlines) == 0) { 702 return; 703 } else { 704 $root->Outlines = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 705 $root->Outlines->Type = new Zend_Pdf_Element_Name('Outlines'); 706 $updateOutlinesNavigation = true; 707 } 708 } else { 709 $updateOutlinesNavigation = false; 710 if (count($this->_originalOutlines) != count($this->outlines)) { 711 // If original and current outlines arrays have different size then outlines list was updated 712 $updateOutlinesNavigation = true; 713 } else if ( !(array_keys($this->_originalOutlines) === array_keys($this->outlines)) ) { 714 // If original and current outlines arrays have different keys (with a glance to an order) then outlines list was updated 715 $updateOutlinesNavigation = true; 716 } else { 717 foreach ($this->outlines as $key => $outline) { 718 if ($this->_originalOutlines[$key] !== $outline) { 719 $updateOutlinesNavigation = true; 720 } 721 } 722 } 723 } 724 725 $lastOutline = null; 726 $openOutlinesCount = 0; 727 if ($updateOutlinesNavigation) { 728 $root->Outlines->touch(); 729 $root->Outlines->First = null; 730 731 foreach ($this->outlines as $outline) { 732 if ($lastOutline === null) { 733 // First pass. Update Outlines dictionary First entry using corresponding value 734 $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines); 735 $root->Outlines->First = $lastOutline; 736 } else { 737 // Update previous outline dictionary Next entry (Prev is updated within dumpOutline() method) 738 $currentOutlineDictionary = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline); 739 $lastOutline->Next = $currentOutlineDictionary; 740 $lastOutline = $currentOutlineDictionary; 741 } 742 $openOutlinesCount += $outline->openOutlinesCount(); 743 } 744 745 $root->Outlines->Last = $lastOutline; 746 } else { 747 foreach ($this->outlines as $outline) { 748 $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline); 749 $openOutlinesCount += $outline->openOutlinesCount(); 750 } 751 } 752 753 if ($openOutlinesCount != $this->_originalOpenOutlinesCount) { 754 $root->Outlines->touch; 755 $root->Outlines->Count = new Zend_Pdf_Element_Numeric($openOutlinesCount); 756 } 757 } 758 759 /** 760 * Create page object, attached to the PDF document. 761 * Method signatures: 762 * 763 * 1. Create new page with a specified pagesize. 764 * If $factory is null then it will be created and page must be attached to the document to be 765 * included into output. 766 * --------------------------------------------------------- 767 * new Zend_Pdf_Page(string $pagesize); 768 * --------------------------------------------------------- 769 * 770 * 2. Create new page with a specified pagesize (in default user space units). 771 * If $factory is null then it will be created and page must be attached to the document to be 772 * included into output. 773 * --------------------------------------------------------- 774 * new Zend_Pdf_Page(numeric $width, numeric $height); 775 * --------------------------------------------------------- 776 * 777 * @param mixed $param1 778 * @param mixed $param2 779 * @return Zend_Pdf_Page 780 */ 781 public function newPage($param1, $param2 = null) 782 { 783 require_once 'Zend/Pdf/Page.php'; 784 if ($param2 === null) { 785 return new Zend_Pdf_Page($param1, $this->_objFactory); 786 } else { 787 return new Zend_Pdf_Page($param1, $param2, $this->_objFactory); 788 } 789 } 790 791 /** 792 * Return the document-level Metadata 793 * or null Metadata stream is not presented 794 * 795 * @return string 796 */ 797 public function getMetadata() 798 { 799 if ($this->_trailer->Root->Metadata !== null) { 800 return $this->_trailer->Root->Metadata->value; 801 } else { 802 return null; 803 } 804 } 805 806 /** 807 * Sets the document-level Metadata (mast be valid XMP document) 808 * 809 * @param string $metadata 810 */ 811 public function setMetadata($metadata) 812 { 813 $metadataObject = $this->_objFactory->newStreamObject($metadata); 814 $metadataObject->dictionary->Type = new Zend_Pdf_Element_Name('Metadata'); 815 $metadataObject->dictionary->Subtype = new Zend_Pdf_Element_Name('XML'); 816 817 $this->_trailer->Root->Metadata = $metadataObject; 818 $this->_trailer->Root->touch(); 819 } 820 821 /** 822 * Return the document-level JavaScript 823 * or null if there is no JavaScript for this document 824 * 825 * @return string 826 */ 827 public function getJavaScript() 828 { 829 return $this->_javaScript; 830 } 831 832 /** 833 * Get open Action 834 * Returns Zend_Pdf_Target (Zend_Pdf_Destination or Zend_Pdf_Action object) 835 * 836 * @return Zend_Pdf_Target 837 */ 838 public function getOpenAction() 839 { 840 if ($this->_trailer->Root->OpenAction !== null) { 841 require_once 'Zend/Pdf/Target.php'; 842 return Zend_Pdf_Target::load($this->_trailer->Root->OpenAction); 843 } else { 844 return null; 845 } 846 } 847 848 /** 849 * Set open Action which is actually Zend_Pdf_Destination or Zend_Pdf_Action object 850 * 851 * @param Zend_Pdf_Target $openAction 852 * @returns Zend_Pdf 853 */ 854 public function setOpenAction(Zend_Pdf_Target $openAction = null) 855 { 856 $root = $this->_trailer->Root; 857 $root->touch(); 858 859 if ($openAction === null) { 860 $root->OpenAction = null; 861 } else { 862 $root->OpenAction = $openAction->getResource(); 863 864 if ($openAction instanceof Zend_Pdf_Action) { 865 $openAction->dumpAction($this->_objFactory); 866 } 867 } 868 869 return $this; 870 } 871 872 /** 873 * Return an associative array containing all the named destinations (or GoTo actions) in the PDF. 874 * Named targets can be used to reference from outside 875 * the PDF, ex: 'http://www.something.com/mydocument.pdf#MyAction' 876 * 877 * @return array 878 */ 879 public function getNamedDestinations() 880 { 881 return $this->_namedTargets; 882 } 883 884 /** 885 * Return specified named destination 886 * 887 * @param string $name 888 * @return Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo 889 */ 890 public function getNamedDestination($name) 891 { 892 if (isset($this->_namedTargets[$name])) { 893 return $this->_namedTargets[$name]; 894 } else { 895 return null; 896 } 897 } 898 899 /** 900 * Set specified named destination 901 * 902 * @param string $name 903 * @param Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo $target 904 */ 905 public function setNamedDestination($name, $destination = null) 906 { 907 if ($destination !== null && 908 !$destination instanceof Zend_Pdf_Action_GoTo && 909 !$destination instanceof Zend_Pdf_Destination_Explicit) { 910 require_once 'Zend/Pdf/Exception.php'; 911 throw new Zend_Pdf_Exception('PDF named destination must refer an explicit destination or a GoTo PDF action.'); 912 } 913 914 if ($destination !== null) { 915 $this->_namedTargets[$name] = $destination; 916 } else { 917 unset($this->_namedTargets[$name]); 918 } 919 } 920 921 /** 922 * Pages collection hash: 923 * <page dictionary object hash id> => Zend_Pdf_Page 924 * 925 * @var SplObjectStorage 926 */ 927 protected $_pageReferences = null; 928 929 /** 930 * Pages collection hash: 931 * <page number> => Zend_Pdf_Page 932 * 933 * @var array 934 */ 935 protected $_pageNumbers = null; 936 937 /** 938 * Refresh page collection hashes 939 * 940 * @return Zend_Pdf 941 */ 942 protected function _refreshPagesHash() 943 { 944 $this->_pageReferences = array(); 945 $this->_pageNumbers = array(); 946 $count = 1; 947 foreach ($this->pages as $page) { 948 $pageDictionaryHashId = spl_object_hash($page->getPageDictionary()->getObject()); 949 $this->_pageReferences[$pageDictionaryHashId] = $page; 950 $this->_pageNumbers[$count++] = $page; 951 } 952 953 return $this; 954 } 955 956 /** 957 * Resolve destination. 958 * 959 * Returns Zend_Pdf_Page page object or null if destination is not found within PDF document. 960 * 961 * @param Zend_Pdf_Destination $destination Destination to resolve 962 * @param boolean $refreshPagesHash Refresh page collection hashes before processing 963 * @return Zend_Pdf_Page|null 964 * @throws Zend_Pdf_Exception 965 */ 966 public function resolveDestination(Zend_Pdf_Destination $destination, $refreshPageCollectionHashes = true) 967 { 968 if ($this->_pageReferences === null || $refreshPageCollectionHashes) { 969 $this->_refreshPagesHash(); 970 } 971 972 if ($destination instanceof Zend_Pdf_Destination_Named) { 973 if (!isset($this->_namedTargets[$destination->getName()])) { 974 return null; 975 } 976 $destination = $this->getNamedDestination($destination->getName()); 977 978 if ($destination instanceof Zend_Pdf_Action) { 979 if (!$destination instanceof Zend_Pdf_Action_GoTo) { 980 return null; 981 } 982 $destination = $destination->getDestination(); 983 } 984 985 if (!$destination instanceof Zend_Pdf_Destination_Explicit) { 986 require_once 'Zend/Pdf/Exception.php'; 987 throw new Zend_Pdf_Exception('Named destination target has to be an explicit destination.'); 988 } 989 } 990 991 // Named target is an explicit destination 992 $pageElement = $destination->getResource()->items[0]; 993 994 if ($pageElement->getType() == Zend_Pdf_Element::TYPE_NUMERIC) { 995 // Page reference is a PDF number 996 if (!isset($this->_pageNumbers[$pageElement->value])) { 997 return null; 998 } 999 1000 return $this->_pageNumbers[$pageElement->value]; 1001 } 1002 1003 // Page reference is a PDF page dictionary reference 1004 $pageDictionaryHashId = spl_object_hash($pageElement->getObject()); 1005 if (!isset($this->_pageReferences[$pageDictionaryHashId])) { 1006 return null; 1007 } 1008 return $this->_pageReferences[$pageDictionaryHashId]; 1009 } 1010 1011 /** 1012 * Walk through action and its chained actions tree and remove nodes 1013 * if they are GoTo actions with an unresolved target. 1014 * 1015 * Returns null if root node is deleted or updated action overwise. 1016 * 1017 * @todo Give appropriate name and make method public 1018 * 1019 * @param Zend_Pdf_Action $action 1020 * @param boolean $refreshPagesHash Refresh page collection hashes before processing 1021 * @return Zend_Pdf_Action|null 1022 */ 1023 protected function _cleanUpAction(Zend_Pdf_Action $action, $refreshPageCollectionHashes = true) 1024 { 1025 if ($this->_pageReferences === null || $refreshPageCollectionHashes) { 1026 $this->_refreshPagesHash(); 1027 } 1028 1029 // Named target is an action 1030 if ($action instanceof Zend_Pdf_Action_GoTo && 1031 $this->resolveDestination($action->getDestination(), false) === null) { 1032 // Action itself is a GoTo action with an unresolved destination 1033 return null; 1034 } 1035 1036 // Walk through child actions 1037 $iterator = new RecursiveIteratorIterator($action, RecursiveIteratorIterator::SELF_FIRST); 1038 1039 $actionsToClean = array(); 1040 $deletionCandidateKeys = array(); 1041 foreach ($iterator as $chainedAction) { 1042 if ($chainedAction instanceof Zend_Pdf_Action_GoTo && 1043 $this->resolveDestination($chainedAction->getDestination(), false) === null) { 1044 // Some child action is a GoTo action with an unresolved destination 1045 // Mark it as a candidate for deletion 1046 $actionsToClean[] = $iterator->getSubIterator(); 1047 $deletionCandidateKeys[] = $iterator->getSubIterator()->key(); 1048 } 1049 } 1050 foreach ($actionsToClean as $id => $action) { 1051 unset($action->next[$deletionCandidateKeys[$id]]); 1052 } 1053 1054 return $action; 1055 } 1056 1057 /** 1058 * Extract fonts attached to the document 1059 * 1060 * returns array of Zend_Pdf_Resource_Font_Extracted objects 1061 * 1062 * @return array 1063 * @throws Zend_Pdf_Exception 1064 */ 1065 public function extractFonts() 1066 { 1067 $fontResourcesUnique = array(); 1068 foreach ($this->pages as $page) { 1069 $pageResources = $page->extractResources(); 1070 1071 if ($pageResources->Font === null) { 1072 // Page doesn't contain have any font reference 1073 continue; 1074 } 1075 1076 $fontResources = $pageResources->Font; 1077 1078 foreach ($fontResources->getKeys() as $fontResourceName) { 1079 $fontDictionary = $fontResources->$fontResourceName; 1080 1081 if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference || 1082 $fontDictionary instanceof Zend_Pdf_Element_Object) ) { 1083 require_once 'Zend/Pdf/Exception.php'; 1084 throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.'); 1085 } 1086 1087 $fontResourcesUnique[spl_object_hash($fontDictionary->getObject())] = $fontDictionary; 1088 } 1089 } 1090 1091 $fonts = array(); 1092 require_once 'Zend/Pdf/Exception.php'; 1093 foreach ($fontResourcesUnique as $resourceId => $fontDictionary) { 1094 try { 1095 // Try to extract font 1096 require_once 'Zend/Pdf/Resource/Font/Extracted.php'; 1097 $extractedFont = new Zend_Pdf_Resource_Font_Extracted($fontDictionary); 1098 1099 $fonts[$resourceId] = $extractedFont; 1100 } catch (Zend_Pdf_Exception $e) { 1101 if ($e->getMessage() != 'Unsupported font type.') { 1102 throw $e; 1103 } 1104 } 1105 } 1106 1107 return $fonts; 1108 } 1109 1110 /** 1111 * Extract font attached to the page by specific font name 1112 * 1113 * $fontName should be specified in UTF-8 encoding 1114 * 1115 * @return Zend_Pdf_Resource_Font_Extracted|null 1116 * @throws Zend_Pdf_Exception 1117 */ 1118 public function extractFont($fontName) 1119 { 1120 $fontResourcesUnique = array(); 1121 require_once 'Zend/Pdf/Exception.php'; 1122 foreach ($this->pages as $page) { 1123 $pageResources = $page->extractResources(); 1124 1125 if ($pageResources->Font === null) { 1126 // Page doesn't contain have any font reference 1127 continue; 1128 } 1129 1130 $fontResources = $pageResources->Font; 1131 1132 foreach ($fontResources->getKeys() as $fontResourceName) { 1133 $fontDictionary = $fontResources->$fontResourceName; 1134 1135 if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference || 1136 $fontDictionary instanceof Zend_Pdf_Element_Object) ) { 1137 require_once 'Zend/Pdf/Exception.php'; 1138 throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.'); 1139 } 1140 1141 $resourceId = spl_object_hash($fontDictionary->getObject()); 1142 if (isset($fontResourcesUnique[$resourceId])) { 1143 continue; 1144 } else { 1145 // Mark resource as processed 1146 $fontResourcesUnique[$resourceId] = 1; 1147 } 1148 1149 if ($fontDictionary->BaseFont->value != $fontName) { 1150 continue; 1151 } 1152 1153 try { 1154 // Try to extract font 1155 require_once 'Zend/Pdf/Resource/Font/Extracted.php'; 1156 return new Zend_Pdf_Resource_Font_Extracted($fontDictionary); 1157 } catch (Zend_Pdf_Exception $e) { 1158 if ($e->getMessage() != 'Unsupported font type.') { 1159 throw $e; 1160 } 1161 // Continue searhing 1162 } 1163 } 1164 } 1165 1166 return null; 1167 } 1168 1169 /** 1170 * Render the completed PDF to a string. 1171 * If $newSegmentOnly is true, then only appended part of PDF is returned. 1172 * 1173 * @param boolean $newSegmentOnly 1174 * @param resource $outputStream 1175 * @return string 1176 * @throws Zend_Pdf_Exception 1177 */ 1178 public function render($newSegmentOnly = false, $outputStream = null) 1179 { 1180 // Save document properties if necessary 1181 if ($this->properties != $this->_originalProperties) { 1182 $docInfo = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary()); 1183 1184 foreach ($this->properties as $key => $value) { 1185 switch ($key) { 1186 case 'Trapped': 1187 switch ($value) { 1188 case true: 1189 $docInfo->$key = new Zend_Pdf_Element_Name('True'); 1190 break; 1191 1192 case false: 1193 $docInfo->$key = new Zend_Pdf_Element_Name('False'); 1194 break; 1195 1196 case null: 1197 $docInfo->$key = new Zend_Pdf_Element_Name('Unknown'); 1198 break; 1199 1200 default: 1201 require_once 'Zend/Pdf/Exception.php'; 1202 throw new Zend_Pdf_Exception('Wrong Trapped document property vale: \'' . $value . '\'. Only true, false and null values are allowed.'); 1203 break; 1204 } 1205 1206 case 'CreationDate': 1207 // break intentionally omitted 1208 case 'ModDate': 1209 $docInfo->$key = new Zend_Pdf_Element_String((string)$value); 1210 break; 1211 1212 case 'Title': 1213 // break intentionally omitted 1214 case 'Author': 1215 // break intentionally omitted 1216 case 'Subject': 1217 // break intentionally omitted 1218 case 'Keywords': 1219 // break intentionally omitted 1220 case 'Creator': 1221 // break intentionally omitted 1222 case 'Producer': 1223 if (extension_loaded('mbstring') === true) { 1224 $detected = mb_detect_encoding($value); 1225 if ($detected !== 'ASCII') { 1226 $value = chr(254) . chr(255) . mb_convert_encoding($value, 'UTF-16', $detected); 1227 } 1228 } 1229 $docInfo->$key = new Zend_Pdf_Element_String((string)$value); 1230 break; 1231 1232 default: 1233 // Set property using PDF type based on PHP type 1234 $docInfo->$key = Zend_Pdf_Element::phpToPdf($value); 1235 break; 1236 } 1237 } 1238 1239 $this->_trailer->Info = $docInfo; 1240 } 1241 1242 $this->_dumpPages(); 1243 $this->_dumpNamedDestinations(); 1244 $this->_dumpOutlines(); 1245 1246 // Check, that PDF file was modified 1247 // File is always modified by _dumpPages() now, but future implementations may eliminate this. 1248 if (!$this->_objFactory->isModified()) { 1249 if ($newSegmentOnly) { 1250 // Do nothing, return 1251 return ''; 1252 } 1253 1254 if ($outputStream === null) { 1255 return $this->_trailer->getPDFString(); 1256 } else { 1257 $pdfData = $this->_trailer->getPDFString(); 1258 while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) { 1259 $pdfData = substr($pdfData, $byteCount); 1260 } 1261 1262 return ''; 1263 } 1264 } 1265 1266 // offset (from a start of PDF file) of new PDF file segment 1267 $offset = $this->_trailer->getPDFLength(); 1268 // Last Object number in a list of free objects 1269 $lastFreeObject = $this->_trailer->getLastFreeObject(); 1270 1271 // Array of cross-reference table subsections 1272 $xrefTable = array(); 1273 // Object numbers of first objects in each subsection 1274 $xrefSectionStartNums = array(); 1275 1276 // Last cross-reference table subsection 1277 $xrefSection = array(); 1278 // Dummy initialization of the first element (specail case - header of linked list of free objects). 1279 $xrefSection[] = 0; 1280 $xrefSectionStartNums[] = 0; 1281 // Object number of last processed PDF object. 1282 // Used to manage cross-reference subsections. 1283 // Initialized by zero (specail case - header of linked list of free objects). 1284 $lastObjNum = 0; 1285 1286 if ($outputStream !== null) { 1287 if (!$newSegmentOnly) { 1288 $pdfData = $this->_trailer->getPDFString(); 1289 while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) { 1290 $pdfData = substr($pdfData, $byteCount); 1291 } 1292 } 1293 } else { 1294 $pdfSegmentBlocks = ($newSegmentOnly) ? array() : array($this->_trailer->getPDFString()); 1295 } 1296 1297 // Iterate objects to create new reference table 1298 foreach ($this->_objFactory->listModifiedObjects() as $updateInfo) { 1299 $objNum = $updateInfo->getObjNum(); 1300 1301 if ($objNum - $lastObjNum != 1) { 1302 // Save cross-reference table subsection and start new one 1303 $xrefTable[] = $xrefSection; 1304 $xrefSection = array(); 1305 $xrefSectionStartNums[] = $objNum; 1306 } 1307 1308 if ($updateInfo->isFree()) { 1309 // Free object cross-reference table entry 1310 $xrefSection[] = sprintf("%010d %05d f \n", $lastFreeObject, $updateInfo->getGenNum()); 1311 $lastFreeObject = $objNum; 1312 } else { 1313 // In-use object cross-reference table entry 1314 $xrefSection[] = sprintf("%010d %05d n \n", $offset, $updateInfo->getGenNum()); 1315 1316 $pdfBlock = $updateInfo->getObjectDump(); 1317 $offset += strlen($pdfBlock); 1318 1319 if ($outputStream === null) { 1320 $pdfSegmentBlocks[] = $pdfBlock; 1321 } else { 1322 while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) { 1323 $pdfBlock = substr($pdfBlock, $byteCount); 1324 } 1325 } 1326 } 1327 $lastObjNum = $objNum; 1328 } 1329 // Save last cross-reference table subsection 1330 $xrefTable[] = $xrefSection; 1331 1332 // Modify first entry (specail case - header of linked list of free objects). 1333 $xrefTable[0][0] = sprintf("%010d 65535 f \n", $lastFreeObject); 1334 1335 $xrefTableStr = "xref\n"; 1336 foreach ($xrefTable as $sectId => $xrefSection) { 1337 $xrefTableStr .= sprintf("%d %d \n", $xrefSectionStartNums[$sectId], count($xrefSection)); 1338 foreach ($xrefSection as $xrefTableEntry) { 1339 $xrefTableStr .= $xrefTableEntry; 1340 } 1341 } 1342 1343 $this->_trailer->Size->value = $this->_objFactory->getObjectCount(); 1344 1345 $pdfBlock = $xrefTableStr 1346 . $this->_trailer->toString() 1347 . "startxref\n" . $offset . "\n" 1348 . "%%EOF\n"; 1349 1350 $this->_objFactory->cleanEnumerationShiftCache(); 1351 1352 if ($outputStream === null) { 1353 $pdfSegmentBlocks[] = $pdfBlock; 1354 1355 return implode('', $pdfSegmentBlocks); 1356 } else { 1357 while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) { 1358 $pdfBlock = substr($pdfBlock, $byteCount); 1359 } 1360 1361 return ''; 1362 } 1363 } 1364 1365 1366 /** 1367 * Set the document-level JavaScript 1368 * 1369 * @param string $javascript 1370 */ 1371 public function setJavaScript($javascript) 1372 { 1373 $this->_javaScript = $javascript; 1374 } 1375 1376 1377 /** 1378 * Convert date to PDF format (it's close to ASN.1 (Abstract Syntax Notation 1379 * One) defined in ISO/IEC 8824). 1380 * 1381 * @todo This really isn't the best location for this method. It should 1382 * probably actually exist as Zend_Pdf_Element_Date or something like that. 1383 * 1384 * @todo Address the following E_STRICT issue: 1385 * PHP Strict Standards: date(): It is not safe to rely on the system's 1386 * timezone settings. Please use the date.timezone setting, the TZ 1387 * environment variable or the date_default_timezone_set() function. In 1388 * case you used any of those methods and you are still getting this 1389 * warning, you most likely misspelled the timezone identifier. 1390 * 1391 * @param integer $timestamp (optional) If omitted, uses the current time. 1392 * @return string 1393 */ 1394 public static function pdfDate($timestamp = null) 1395 { 1396 if ($timestamp === null) { 1397 $date = date('\D\:YmdHisO'); 1398 } else { 1399 $date = date('\D\:YmdHisO', $timestamp); 1400 } 1401 return substr_replace($date, '\'', -2, 0) . '\''; 1402 } 1403 1404}