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.
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
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 */
27 require_once 'Zend/Pdf/Page.php';
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';
45 require_once 'Zend/Pdf/Image.php';
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';
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)
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
80 /**** Class Constants ****/
83 * Version number of generated PDF documents.
85 const PDF_VERSION
= '1.4';
90 const PDF_HEADER
= "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
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)
120 public $properties = array();
123 * Original properties set.
125 * Used for tracking properties changes
129 protected $_originalProperties = array();
132 * Document level javascript
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();
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
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;
192 * It's not used, but has to be destroyed only with Zend_Pdf object
194 * @var Zend_Pdf_Parser
200 * List of inheritable attributesfor pages tree
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
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
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);
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
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
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);
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']) {
325 $this->properties
['Trapped'] = true;
329 $this->properties
['Trapped'] = false;
333 $this->properties
['Trapped'] = null;
337 // Wrong property value
343 $this->_originalProperties
= $this->properties
;
346 $this->_pdfHeaderVersion
= Zend_Pdf
::PDF_VERSION
;
348 $trailerDictionary = new Zend_Pdf_Element_Dictionary();
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;
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.
390 public function revisions()
393 $currentTrailer = $this->_trailer
;
395 while ($currentTrailer->getPrev() !== null && $currentTrailer->getPrev()->Root
!== null ) {
397 $currentTrailer = $currentTrailer->getPrev();
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();
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)) {
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];
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', '>');
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);
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) {
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) {
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]);
607 require_once 'Zend/Pdf/Exception.php';
608 throw new Zend_Pdf_Exception('Wrong type of named targed (\'' . get_class($namedTarget) . '\').');
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);
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);
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();
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) {
686 $root->Names
= $this->_objFactory
->newObject(new Zend_Pdf_Element_Dictionary());
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) {
704 $root->Outlines
= $this->_objFactory
->newObject(new Zend_Pdf_Element_Dictionary());
705 $root->Outlines
->Type
= new Zend_Pdf_Element_Name('Outlines');
706 $updateOutlinesNavigation = true;
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;
717 foreach ($this->outlines
as $key => $outline) {
718 if ($this->_originalOutlines
[$key] !== $outline) {
719 $updateOutlinesNavigation = true;
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;
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;
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.
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
);
787 return new Zend_Pdf_Page($param1, $param2, $this->_objFactory
);
792 * Return the document-level Metadata
793 * or null Metadata stream is not presented
797 public function getMetadata()
799 if ($this->_trailer
->Root
->Metadata
!== null) {
800 return $this->_trailer
->Root
->Metadata
->value
;
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
827 public function getJavaScript()
829 return $this->_javaScript
;
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
);
849 * Set open Action which is actually Zend_Pdf_Destination or Zend_Pdf_Action object
851 * @param Zend_Pdf_Target $openAction
854 public function setOpenAction(Zend_Pdf_Target
$openAction = null)
856 $root = $this->_trailer
->Root
;
859 if ($openAction === null) {
860 $root->OpenAction
= null;
862 $root->OpenAction
= $openAction->getResource();
864 if ($openAction instanceof Zend_Pdf_Action
) {
865 $openAction->dumpAction($this->_objFactory
);
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'
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];
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;
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
935 protected $_pageNumbers = null;
938 * Refresh page collection hashes
942 protected function _refreshPagesHash()
944 $this->_pageReferences
= array();
945 $this->_pageNumbers
= array();
947 foreach ($this->pages
as $page) {
948 $pageDictionaryHashId = spl_object_hash($page->getPageDictionary()->getObject());
949 $this->_pageReferences
[$pageDictionaryHashId] = $page;
950 $this->_pageNumbers
[$count++
] = $page;
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()])) {
976 $destination = $this->getNamedDestination($destination->getName());
978 if ($destination instanceof Zend_Pdf_Action
) {
979 if (!$destination instanceof Zend_Pdf_Action_GoTo
) {
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
])) {
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])) {
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
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]]);
1058 * Extract fonts attached to the document
1060 * returns array of Zend_Pdf_Resource_Font_Extracted objects
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
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;
1092 require_once 'Zend/Pdf/Exception.php';
1093 foreach ($fontResourcesUnique as $resourceId => $fontDictionary) {
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.') {
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
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])) {
1145 // Mark resource as processed
1146 $fontResourcesUnique[$resourceId] = 1;
1149 if ($fontDictionary->BaseFont
->value
!= $fontName) {
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.') {
1161 // Continue searhing
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
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) {
1189 $docInfo->$key = new Zend_Pdf_Element_Name('True');
1193 $docInfo->$key = new Zend_Pdf_Element_Name('False');
1197 $docInfo->$key = new Zend_Pdf_Element_Name('Unknown');
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.');
1206 case 'CreationDate':
1207 // break intentionally omitted
1209 $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
1213 // break intentionally omitted
1215 // break intentionally omitted
1217 // break intentionally omitted
1219 // break intentionally omitted
1221 // break intentionally omitted
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);
1233 // Set property using PDF type based on PHP type
1234 $docInfo->$key = Zend_Pdf_Element
::phpToPdf($value);
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
1254 if ($outputStream === null) {
1255 return $this->_trailer
->getPDFString();
1257 $pdfData = $this->_trailer
->getPDFString();
1258 while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) {
1259 $pdfData = substr($pdfData, $byteCount);
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).
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).
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);
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;
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;
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"
1350 $this->_objFactory
->cleanEnumerationShiftCache();
1352 if ($outputStream === null) {
1353 $pdfSegmentBlocks[] = $pdfBlock;
1355 return implode('', $pdfSegmentBlocks);
1357 while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
1358 $pdfBlock = substr($pdfBlock, $byteCount);
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.
1394 public static function pdfDate($timestamp = null)
1396 if ($timestamp === null) {
1397 $date = date('\D\:YmdHisO');
1399 $date = date('\D\:YmdHisO', $timestamp);
1401 return substr_replace($date, '\'', -2, 0) . '\'';