[ZF-10089] Zend_Log
[zend/radio.git] / library / Zend / Pdf.php
blob49f16b4fb5fcd780f01ff6557b41fab147ee42c6
1 <?php
2 /**
3 * Zend Framework
5 * LICENSE
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.
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$
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 */
26 /** Zend_Pdf_Page */
27 require_once 'Zend/Pdf/Page.php';
29 /** Zend_Pdf_Style */
30 require_once 'Zend/Pdf/Style.php';
32 /** Zend_Pdf_Color_GrayScale */
33 require_once 'Zend/Pdf/Color/GrayScale.php';
35 /** Zend_Pdf_Color_Rgb */
36 require_once 'Zend/Pdf/Color/Rgb.php';
38 /** Zend_Pdf_Color_Cmyk */
39 require_once 'Zend/Pdf/Color/Cmyk.php';
41 /** Zend_Pdf_Color_Html */
42 require_once 'Zend/Pdf/Color/Html.php';
44 /** Zend_Pdf_Image */
45 require_once 'Zend/Pdf/Image.php';
47 /** Zend_Pdf_Font */
48 require_once 'Zend/Pdf/Font.php';
51 /** Internally used classes */
52 require_once 'Zend/Pdf/Element.php';
53 require_once 'Zend/Pdf/Element/Array.php';
54 require_once 'Zend/Pdf/Element/String/Binary.php';
55 require_once 'Zend/Pdf/Element/Boolean.php';
56 require_once 'Zend/Pdf/Element/Dictionary.php';
57 require_once 'Zend/Pdf/Element/Name.php';
58 require_once 'Zend/Pdf/Element/Null.php';
59 require_once 'Zend/Pdf/Element/Numeric.php';
60 require_once 'Zend/Pdf/Element/String.php';
63 /**
64 * General entity which describes PDF document.
65 * It implements document abstraction with a document level operations.
67 * Class is used to create new PDF document or load existing document.
68 * See details in a class constructor description
70 * Class agregates document level properties and entities (pages, bookmarks,
71 * document level actions, attachments, form object, etc)
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
78 class Zend_Pdf
80 /**** Class Constants ****/
82 /**
83 * Version number of generated PDF documents.
85 const PDF_VERSION = '1.4';
87 /**
88 * PDF file header.
90 const PDF_HEADER = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
94 /**
95 * Pages collection
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.
101 * @var array - array of Zend_Pdf_Page object
103 public $pages = array();
106 * Document properties
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.
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)
118 * @var array
120 public $properties = array();
123 * Original properties set.
125 * Used for tracking properties changes
127 * @var array
129 protected $_originalProperties = array();
132 * Document level javascript
134 * @var string
136 protected $_javaScript = null;
139 * Document named destinations or "GoTo..." actions, used to refer
140 * document parts from outside PDF
142 * @var array - array of Zend_Pdf_Target objects
144 protected $_namedTargets = array();
147 * Document outlines
149 * @var array - array of Zend_Pdf_Outline objects
151 public $outlines = array();
154 * Original document outlines list
155 * Used to track outlines update
157 * @var array - array of Zend_Pdf_Outline objects
159 protected $_originalOutlines = array();
162 * Original document outlines open elements count
163 * Used to track outlines update
165 * @var integer
167 protected $_originalOpenOutlinesCount = 0;
170 * Pdf trailer (last or just created)
172 * @var Zend_Pdf_Trailer
174 protected $_trailer = null;
177 * PDF objects factory.
179 * @var Zend_Pdf_ElementFactory_Interface
181 protected $_objFactory = null;
184 * Memory manager for stream objects
186 * @var Zend_Memory_Manager|null
188 protected static $_memoryManager = null;
191 * Pdf file parser.
192 * It's not used, but has to be destroyed only with Zend_Pdf object
194 * @var Zend_Pdf_Parser
196 protected $_parser;
200 * List of inheritable attributesfor pages tree
202 * @var array
204 protected static $_inheritableAttributes = array('Resources', 'MediaBox', 'CropBox', 'Rotate');
207 * Request used memory manager
209 * @return Zend_Memory_Manager
211 static public function getMemoryManager()
213 if (self::$_memoryManager === null) {
214 require_once 'Zend/Memory.php';
215 self::$_memoryManager = Zend_Memory::factory('none');
218 return self::$_memoryManager;
222 * Set user defined memory manager
224 * @param Zend_Memory_Manager $memoryManager
226 static public function setMemoryManager(Zend_Memory_Manager $memoryManager)
228 self::$_memoryManager = $memoryManager;
233 * Create new PDF document from a $source string
235 * @param string $source
236 * @param integer $revision
237 * @return Zend_Pdf
239 public static function parse(&$source = null, $revision = null)
241 return new Zend_Pdf($source, $revision);
245 * Load PDF document from a file
247 * @param string $source
248 * @param integer $revision
249 * @return Zend_Pdf
251 public static function load($source = null, $revision = null)
253 return new Zend_Pdf($source, $revision, true);
257 * Render PDF document and save it.
259 * If $updateOnly is true, then it only appends new section to the end of file.
261 * @param string $filename
262 * @param boolean $updateOnly
263 * @throws Zend_Pdf_Exception
265 public function save($filename, $updateOnly = false)
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." );
272 $this->render($updateOnly, $file);
274 fclose($file);
278 * Creates or loads PDF document.
280 * If $source is null, then it creates a new document.
282 * If $source is a string and $load is false, then it loads document
283 * from a binary string.
285 * If $source is a string and $load is true, then it loads document
286 * from a file.
288 * $revision used to roll back document to specified version
289 * (0 - current version, 1 - previous version, 2 - ...)
291 * @param string $source - PDF file to load
292 * @param integer $revision
293 * @throws Zend_Pdf_Exception
294 * @return Zend_Pdf
296 public function __construct($source = null, $revision = null, $load = false)
298 require_once 'Zend/Pdf/ElementFactory.php';
299 $this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1);
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');
310 if ($revision !== null) {
311 $this->rollback($revision);
312 } else {
313 $this->_loadPages($this->_trailer->Root->Pages);
316 $this->_loadNamedDestinations($this->_trailer->Root, $this->_parser->getPDFVersion());
317 $this->_loadOutlines($this->_trailer->Root);
319 if ($this->_trailer->Info !== null) {
320 $this->properties = $this->_trailer->Info->toPhp();
322 if (isset($this->properties['Trapped'])) {
323 switch ($this->properties['Trapped']) {
324 case 'True':
325 $this->properties['Trapped'] = true;
326 break;
328 case 'False':
329 $this->properties['Trapped'] = false;
330 break;
332 case 'Unknown':
333 $this->properties['Trapped'] = null;
334 break;
336 default:
337 // Wrong property value
338 // Do nothing
339 break;
343 $this->_originalProperties = $this->properties;
345 } else {
346 $this->_pdfHeaderVersion = Zend_Pdf::PDF_VERSION;
348 $trailerDictionary = new Zend_Pdf_Element_Dictionary();
351 * Document id
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
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);
361 $trailerDictionary->Size = new Zend_Pdf_Element_Numeric(0);
363 require_once 'Zend/Pdf/Trailer/Generator.php';
364 $this->_trailer = new Zend_Pdf_Trailer_Generator($trailerDictionary);
367 * Document catalog indirect object.
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;
375 * Pages container
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;
386 * Retrive number of revisions.
388 * @return integer
390 public function revisions()
392 $revisions = 1;
393 $currentTrailer = $this->_trailer;
395 while ($currentTrailer->getPrev() !== null && $currentTrailer->getPrev()->Root !== null ) {
396 $revisions++;
397 $currentTrailer = $currentTrailer->getPrev();
400 return $revisions++;
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.
408 * @param integer $steps
410 public function rollback($steps)
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;
419 $this->_objFactory->setObjectCount($this->_trailer->Size->value);
421 // Mark content as modified to force new trailer generation at render time
422 $this->_trailer->Root->touch();
424 $this->pages = array();
425 $this->_loadPages($this->_trailer->Root->Pages);
430 * Load pages recursively
432 * @param Zend_Pdf_Element_Reference $pages
433 * @param array|null $attributes
435 protected function _loadPages(Zend_Pdf_Element_Reference $pages, $attributes = array())
437 if ($pages->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
438 require_once 'Zend/Pdf/Exception.php';
439 throw new Zend_Pdf_Exception('Wrong argument');
442 foreach ($pages->getKeys() as $property) {
443 if (in_array($property, self::$_inheritableAttributes)) {
444 $attributes[$property] = $pages->$property;
445 $pages->$property = null;
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)) {
457 * Important note.
458 * If any attribute or dependant object is an indirect object, then it's still
459 * shared between pages.
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]);
470 require_once 'Zend/Pdf/Page.php';
471 $this->pages[] = new Zend_Pdf_Page($child, $this->_objFactory);
477 * Load named destinations recursively
479 * @param Zend_Pdf_Element_Reference $root Document catalog entry
480 * @param string $pdfHeaderVersion
481 * @throws Zend_Pdf_Exception
483 protected function _loadNamedDestinations(Zend_Pdf_Element_Reference $root, $pdfHeaderVersion)
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', '>');
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);
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.');
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);
519 * Load outlines recursively
521 * @param Zend_Pdf_Element_Reference $root Document catalog entry
523 protected function _loadOutlines(Zend_Pdf_Element_Reference $root)
525 if ($root->Outlines === null) {
526 return;
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.');
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.');
539 if ($root->Outlines->First === null) {
540 return;
543 $outlineDictionary = $root->Outlines->First;
544 $processedDictionaries = new SplObjectStorage();
545 while ($outlineDictionary !== null && !$processedDictionaries->contains($outlineDictionary)) {
546 $processedDictionaries->attach($outlineDictionary);
548 require_once 'Zend/Pdf/Outline/Loaded.php';
549 $this->outlines[] = new Zend_Pdf_Outline_Loaded($outlineDictionary);
551 $outlineDictionary = $outlineDictionary->Next;
554 $this->_originalOutlines = $this->outlines;
556 if ($root->Outlines->Count !== null) {
557 $this->_originalOpenOutlinesCount = $root->Outlines->Count->value;
562 * Orginize pages to tha pages tree structure.
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
567 * @todo Dump pages as a balanced tree instead of a plain set.
569 protected function _dumpPages()
571 $root = $this->_trailer->Root;
572 $pagesContainer = $root->Pages;
574 $pagesContainer->touch();
575 $pagesContainer->Kids->items = array();
577 foreach ($this->pages as $page ) {
578 $page->render($this->_objFactory);
580 $pageDictionary = $page->getPageDictionary();
581 $pageDictionary->touch();
582 $pageDictionary->Parent = $pagesContainer;
584 $pagesContainer->Kids->items[] = $pageDictionary;
587 $this->_refreshPagesHash();
589 $pagesContainer->Count->touch();
590 $pagesContainer->Count->value = count($this->pages);
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]);
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]);
606 } else {
607 require_once 'Zend/Pdf/Exception.php';
608 throw new Zend_Pdf_Exception('Wrong type of named targed (\'' . get_class($namedTarget) . '\').');
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();
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);
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);
630 } else {
631 require_once 'Zend/Pdf/Exception.php';
632 throw new Zend_Pdf_Exception('Wrong outline target.');
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);
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);
650 } else {
651 require_once 'Zend/Pdf/Exception.php';
652 throw new Zend_Pdf_Exception('OpenAction has to be either PDF Action or Destination.');
658 * Dump named destinations
660 * @todo Create a balanced tree instead of plain structure.
662 protected function _dumpNamedDestinations()
664 ksort($this->_namedTargets, SORT_STRING);
666 $destArrayItems = array();
667 foreach ($this->_namedTargets as $name => $destination) {
668 $destArrayItems[] = new Zend_Pdf_Element_String($name);
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.');
677 $destArray = $this->_objFactory->newObject(new Zend_Pdf_Element_Array($destArrayItems));
679 $DestTree = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
680 $DestTree->Names = $destArray;
682 $root = $this->_trailer->Root;
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();
690 $root->Names->Dests = $DestTree;
694 * Dump outlines recursively
696 protected function _dumpOutlines()
698 $root = $this->_trailer->Root;
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;
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;
725 $lastOutline = null;
726 $openOutlinesCount = 0;
727 if ($updateOutlinesNavigation) {
728 $root->Outlines->touch();
729 $root->Outlines->First = null;
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;
742 $openOutlinesCount += $outline->openOutlinesCount();
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();
753 if ($openOutlinesCount != $this->_originalOpenOutlinesCount) {
754 $root->Outlines->touch;
755 $root->Outlines->Count = new Zend_Pdf_Element_Numeric($openOutlinesCount);
760 * Create page object, attached to the PDF document.
761 * Method signatures:
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 * ---------------------------------------------------------
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 * ---------------------------------------------------------
777 * @param mixed $param1
778 * @param mixed $param2
779 * @return Zend_Pdf_Page
781 public function newPage($param1, $param2 = null)
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);
792 * Return the document-level Metadata
793 * or null Metadata stream is not presented
795 * @return string
797 public function getMetadata()
799 if ($this->_trailer->Root->Metadata !== null) {
800 return $this->_trailer->Root->Metadata->value;
801 } else {
802 return null;
807 * Sets the document-level Metadata (mast be valid XMP document)
809 * @param string $metadata
811 public function setMetadata($metadata)
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');
817 $this->_trailer->Root->Metadata = $metadataObject;
818 $this->_trailer->Root->touch();
822 * Return the document-level JavaScript
823 * or null if there is no JavaScript for this document
825 * @return string
827 public function getJavaScript()
829 return $this->_javaScript;
833 * Get open Action
834 * Returns Zend_Pdf_Target (Zend_Pdf_Destination or Zend_Pdf_Action object)
836 * @return Zend_Pdf_Target
838 public function getOpenAction()
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;
849 * Set open Action which is actually Zend_Pdf_Destination or Zend_Pdf_Action object
851 * @param Zend_Pdf_Target $openAction
852 * @returns Zend_Pdf
854 public function setOpenAction(Zend_Pdf_Target $openAction = null)
856 $root = $this->_trailer->Root;
857 $root->touch();
859 if ($openAction === null) {
860 $root->OpenAction = null;
861 } else {
862 $root->OpenAction = $openAction->getResource();
864 if ($openAction instanceof Zend_Pdf_Action) {
865 $openAction->dumpAction($this->_objFactory);
869 return $this;
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'
877 * @return array
879 public function getNamedDestinations()
881 return $this->_namedTargets;
885 * Return specified named destination
887 * @param string $name
888 * @return Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo
890 public function getNamedDestination($name)
892 if (isset($this->_namedTargets[$name])) {
893 return $this->_namedTargets[$name];
894 } else {
895 return null;
900 * Set specified named destination
902 * @param string $name
903 * @param Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo $target
905 public function setNamedDestination($name, $destination = null)
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.');
914 if ($destination !== null) {
915 $this->_namedTargets[$name] = $destination;
916 } else {
917 unset($this->_namedTargets[$name]);
922 * Pages collection hash:
923 * <page dictionary object hash id> => Zend_Pdf_Page
925 * @var SplObjectStorage
927 protected $_pageReferences = null;
930 * Pages collection hash:
931 * <page number> => Zend_Pdf_Page
933 * @var array
935 protected $_pageNumbers = null;
938 * Refresh page collection hashes
940 * @return Zend_Pdf
942 protected function _refreshPagesHash()
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;
953 return $this;
957 * Resolve destination.
959 * Returns Zend_Pdf_Page page object or null if destination is not found within PDF document.
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
966 public function resolveDestination(Zend_Pdf_Destination $destination, $refreshPageCollectionHashes = true)
968 if ($this->_pageReferences === null || $refreshPageCollectionHashes) {
969 $this->_refreshPagesHash();
972 if ($destination instanceof Zend_Pdf_Destination_Named) {
973 if (!isset($this->_namedTargets[$destination->getName()])) {
974 return null;
976 $destination = $this->getNamedDestination($destination->getName());
978 if ($destination instanceof Zend_Pdf_Action) {
979 if (!$destination instanceof Zend_Pdf_Action_GoTo) {
980 return null;
982 $destination = $destination->getDestination();
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.');
991 // Named target is an explicit destination
992 $pageElement = $destination->getResource()->items[0];
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;
1000 return $this->_pageNumbers[$pageElement->value];
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;
1008 return $this->_pageReferences[$pageDictionaryHashId];
1012 * Walk through action and its chained actions tree and remove nodes
1013 * if they are GoTo actions with an unresolved target.
1015 * Returns null if root node is deleted or updated action overwise.
1017 * @todo Give appropriate name and make method public
1019 * @param Zend_Pdf_Action $action
1020 * @param boolean $refreshPagesHash Refresh page collection hashes before processing
1021 * @return Zend_Pdf_Action|null
1023 protected function _cleanUpAction(Zend_Pdf_Action $action, $refreshPageCollectionHashes = true)
1025 if ($this->_pageReferences === null || $refreshPageCollectionHashes) {
1026 $this->_refreshPagesHash();
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;
1036 // Walk through child actions
1037 $iterator = new RecursiveIteratorIterator($action, RecursiveIteratorIterator::SELF_FIRST);
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();
1050 foreach ($actionsToClean as $id => $action) {
1051 unset($action->next[$deletionCandidateKeys[$id]]);
1054 return $action;
1058 * Extract fonts attached to the document
1060 * returns array of Zend_Pdf_Resource_Font_Extracted objects
1062 * @return array
1063 * @throws Zend_Pdf_Exception
1065 public function extractFonts()
1067 $fontResourcesUnique = array();
1068 foreach ($this->pages as $page) {
1069 $pageResources = $page->extractResources();
1071 if ($pageResources->Font === null) {
1072 // Page doesn't contain have any font reference
1073 continue;
1076 $fontResources = $pageResources->Font;
1078 foreach ($fontResources->getKeys() as $fontResourceName) {
1079 $fontDictionary = $fontResources->$fontResourceName;
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.');
1087 $fontResourcesUnique[spl_object_hash($fontDictionary->getObject())] = $fontDictionary;
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);
1099 $fonts[$resourceId] = $extractedFont;
1100 } catch (Zend_Pdf_Exception $e) {
1101 if ($e->getMessage() != 'Unsupported font type.') {
1102 throw $e;
1107 return $fonts;
1111 * Extract font attached to the page by specific font name
1113 * $fontName should be specified in UTF-8 encoding
1115 * @return Zend_Pdf_Resource_Font_Extracted|null
1116 * @throws Zend_Pdf_Exception
1118 public function extractFont($fontName)
1120 $fontResourcesUnique = array();
1121 require_once 'Zend/Pdf/Exception.php';
1122 foreach ($this->pages as $page) {
1123 $pageResources = $page->extractResources();
1125 if ($pageResources->Font === null) {
1126 // Page doesn't contain have any font reference
1127 continue;
1130 $fontResources = $pageResources->Font;
1132 foreach ($fontResources->getKeys() as $fontResourceName) {
1133 $fontDictionary = $fontResources->$fontResourceName;
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.');
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;
1149 if ($fontDictionary->BaseFont->value != $fontName) {
1150 continue;
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;
1161 // Continue searhing
1166 return null;
1170 * Render the completed PDF to a string.
1171 * If $newSegmentOnly is true, then only appended part of PDF is returned.
1173 * @param boolean $newSegmentOnly
1174 * @param resource $outputStream
1175 * @return string
1176 * @throws Zend_Pdf_Exception
1178 public function render($newSegmentOnly = false, $outputStream = null)
1180 // Save document properties if necessary
1181 if ($this->properties != $this->_originalProperties) {
1182 $docInfo = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
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;
1192 case false:
1193 $docInfo->$key = new Zend_Pdf_Element_Name('False');
1194 break;
1196 case null:
1197 $docInfo->$key = new Zend_Pdf_Element_Name('Unknown');
1198 break;
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;
1206 case 'CreationDate':
1207 // break intentionally omitted
1208 case 'ModDate':
1209 $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
1210 break;
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);
1229 $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
1230 break;
1232 default:
1233 // Set property using PDF type based on PHP type
1234 $docInfo->$key = Zend_Pdf_Element::phpToPdf($value);
1235 break;
1239 $this->_trailer->Info = $docInfo;
1242 $this->_dumpPages();
1243 $this->_dumpNamedDestinations();
1244 $this->_dumpOutlines();
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 '';
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);
1262 return '';
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();
1271 // Array of cross-reference table subsections
1272 $xrefTable = array();
1273 // Object numbers of first objects in each subsection
1274 $xrefSectionStartNums = array();
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;
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);
1293 } else {
1294 $pdfSegmentBlocks = ($newSegmentOnly) ? array() : array($this->_trailer->getPDFString());
1297 // Iterate objects to create new reference table
1298 foreach ($this->_objFactory->listModifiedObjects() as $updateInfo) {
1299 $objNum = $updateInfo->getObjNum();
1301 if ($objNum - $lastObjNum != 1) {
1302 // Save cross-reference table subsection and start new one
1303 $xrefTable[] = $xrefSection;
1304 $xrefSection = array();
1305 $xrefSectionStartNums[] = $objNum;
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());
1316 $pdfBlock = $updateInfo->getObjectDump();
1317 $offset += strlen($pdfBlock);
1319 if ($outputStream === null) {
1320 $pdfSegmentBlocks[] = $pdfBlock;
1321 } else {
1322 while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
1323 $pdfBlock = substr($pdfBlock, $byteCount);
1327 $lastObjNum = $objNum;
1329 // Save last cross-reference table subsection
1330 $xrefTable[] = $xrefSection;
1332 // Modify first entry (specail case - header of linked list of free objects).
1333 $xrefTable[0][0] = sprintf("%010d 65535 f \n", $lastFreeObject);
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;
1343 $this->_trailer->Size->value = $this->_objFactory->getObjectCount();
1345 $pdfBlock = $xrefTableStr
1346 . $this->_trailer->toString()
1347 . "startxref\n" . $offset . "\n"
1348 . "%%EOF\n";
1350 $this->_objFactory->cleanEnumerationShiftCache();
1352 if ($outputStream === null) {
1353 $pdfSegmentBlocks[] = $pdfBlock;
1355 return implode('', $pdfSegmentBlocks);
1356 } else {
1357 while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
1358 $pdfBlock = substr($pdfBlock, $byteCount);
1361 return '';
1367 * Set the document-level JavaScript
1369 * @param string $javascript
1371 public function setJavaScript($javascript)
1373 $this->_javaScript = $javascript;
1378 * Convert date to PDF format (it's close to ASN.1 (Abstract Syntax Notation
1379 * One) defined in ISO/IEC 8824).
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.
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.
1391 * @param integer $timestamp (optional) If omitted, uses the current time.
1392 * @return string
1394 public static function pdfDate($timestamp = null)
1396 if ($timestamp === null) {
1397 $date = date('\D\:YmdHisO');
1398 } else {
1399 $date = date('\D\:YmdHisO', $timestamp);
1401 return substr_replace($date, '\'', -2, 0) . '\'';