File indexing completed on 2024-04-28 06:00:03

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Pdf
0017  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0018  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0019  * @version    $Id$
0020  */
0021 
0022 
0023 /** User land classes and interfaces turned on by Zend/Pdf.php file inclusion. */
0024 /** @todo Section should be removed with ZF 2.0 release as obsolete            */
0025 
0026 /** Zend_Pdf_Page */
0027 // require_once 'Zend/Pdf/Page.php';
0028 
0029 /** Zend_Pdf_Style */
0030 // require_once 'Zend/Pdf/Style.php';
0031 
0032 /** Zend_Pdf_Color_GrayScale */
0033 // require_once 'Zend/Pdf/Color/GrayScale.php';
0034 
0035 /** Zend_Pdf_Color_Rgb */
0036 // require_once 'Zend/Pdf/Color/Rgb.php';
0037 
0038 /** Zend_Pdf_Color_Cmyk */
0039 // require_once 'Zend/Pdf/Color/Cmyk.php';
0040 
0041 /** Zend_Pdf_Color_Html */
0042 // require_once 'Zend/Pdf/Color/Html.php';
0043 
0044 /** Zend_Pdf_Image */
0045 // require_once 'Zend/Pdf/Image.php';
0046 
0047 /** Zend_Pdf_Font */
0048 // require_once 'Zend/Pdf/Font.php';
0049 
0050 /** Zend_Pdf_Resource_Extractor */
0051 // require_once 'Zend/Pdf/Resource/Extractor.php';
0052 
0053 /** Zend_Pdf_Canvas */
0054 // require_once 'Zend/Pdf/Canvas.php';
0055 
0056 
0057 /** Internally used classes */
0058 // require_once 'Zend/Pdf/Element.php';
0059 // require_once 'Zend/Pdf/Element/Array.php';
0060 // require_once 'Zend/Pdf/Element/String/Binary.php';
0061 // require_once 'Zend/Pdf/Element/Boolean.php';
0062 // require_once 'Zend/Pdf/Element/Dictionary.php';
0063 // require_once 'Zend/Pdf/Element/Name.php';
0064 // require_once 'Zend/Pdf/Element/Null.php';
0065 // require_once 'Zend/Pdf/Element/Numeric.php';
0066 // require_once 'Zend/Pdf/Element/String.php';
0067 
0068 
0069 /**
0070  * General entity which describes PDF document.
0071  * It implements document abstraction with a document level operations.
0072  *
0073  * Class is used to create new PDF document or load existing document.
0074  * See details in a class constructor description
0075  *
0076  * Class agregates document level properties and entities (pages, bookmarks,
0077  * document level actions, attachments, form object, etc)
0078  *
0079  * @category   Zend
0080  * @package    Zend_Pdf
0081  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0082  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0083  */
0084 class Zend_Pdf
0085 {
0086   /**** Class Constants ****/
0087 
0088     /**
0089      * Version number of generated PDF documents.
0090      */
0091     const PDF_VERSION = '1.4';
0092 
0093     /**
0094      * PDF file header.
0095      */
0096     const PDF_HEADER  = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
0097 
0098     /**
0099      * Form field options
0100      */
0101     const PDF_FORM_FIELD_READONLY = 1;
0102     const PDF_FORM_FIELD_REQUIRED = 2;
0103     const PDF_FORM_FIELD_NOEXPORT = 4;
0104 
0105     /**
0106      * Pages collection
0107      *
0108      * @todo implement it as a class, which supports ArrayAccess and Iterator interfaces,
0109      *       to provide incremental parsing and pages tree updating.
0110      *       That will give good performance and memory (PDF size) benefits.
0111      *
0112      * @var array   - array of Zend_Pdf_Page object
0113      */
0114     public $pages = array();
0115 
0116     /**
0117      * Document properties
0118      *
0119      * It's an associative array with PDF meta information, values may
0120      * be string, boolean or float.
0121      * Returned array could be used directly to access, add, modify or remove
0122      * document properties.
0123      *
0124      * Standard document properties: Title (must be set for PDF/X documents), Author,
0125      * Subject, Keywords (comma separated list), Creator (the name of the application,
0126      * that created document, if it was converted from other format), Trapped (must be
0127      * true, false or null, can not be null for PDF/X documents)
0128      *
0129      * @var array
0130      */
0131     public $properties = array();
0132 
0133     /**
0134      * Original properties set.
0135      *
0136      * Used for tracking properties changes
0137      *
0138      * @var array
0139      */
0140     protected $_originalProperties = array();
0141 
0142     /**
0143      * Document level javascript
0144      *
0145      * @var string
0146      */
0147     protected $_javaScript = null;
0148 
0149     /**
0150      * Document named destinations or "GoTo..." actions, used to refer
0151      * document parts from outside PDF
0152      *
0153      * @var array   - array of Zend_Pdf_Target objects
0154      */
0155     protected $_namedTargets = array();
0156 
0157     /**
0158      * Document outlines
0159      *
0160      * @var array - array of Zend_Pdf_Outline objects
0161      */
0162     public $outlines = array();
0163 
0164     /**
0165      * Original document outlines list
0166      * Used to track outlines update
0167      *
0168      * @var array - array of Zend_Pdf_Outline objects
0169      */
0170     protected $_originalOutlines = array();
0171 
0172     /**
0173      * Original document outlines open elements count
0174      * Used to track outlines update
0175      *
0176      * @var integer
0177      */
0178     protected $_originalOpenOutlinesCount = 0;
0179 
0180     /**
0181      * Pdf trailer (last or just created)
0182      *
0183      * @var Zend_Pdf_Trailer
0184      */
0185     protected $_trailer = null;
0186 
0187     /**
0188      * PDF objects factory.
0189      *
0190      * @var Zend_Pdf_ElementFactory_Interface
0191      */
0192     protected $_objFactory = null;
0193 
0194     /**
0195      * Memory manager for stream objects
0196      *
0197      * @var Zend_Memory_Manager|null
0198      */
0199     protected static $_memoryManager = null;
0200 
0201     /**
0202      * Pdf file parser.
0203      * It's not used, but has to be destroyed only with Zend_Pdf object
0204      *
0205      * @var Zend_Pdf_Parser
0206      */
0207     protected $_parser;
0208 
0209     /**
0210      * List of inheritable attributesfor pages tree
0211      *
0212      * @var array
0213      */
0214     protected static $_inheritableAttributes = array('Resources', 'MediaBox', 'CropBox', 'Rotate');
0215 
0216     /**
0217      * List of form fields
0218      *
0219      * @var array - Associative array, key: name of form field, value: Zend_Pdf_Element
0220      */
0221     protected $_formFields = array();
0222 
0223     /**
0224      * True if the object is a newly created PDF document (affects save() method behavior)
0225      * False otherwise
0226      *
0227      * @var boolean
0228      */
0229     protected $_isNewDocument = true;
0230 
0231     /**
0232      * Request used memory manager
0233      *
0234      * @return Zend_Memory_Manager
0235      */
0236     static public function getMemoryManager()
0237     {
0238         if (self::$_memoryManager === null) {
0239             // require_once 'Zend/Memory.php';
0240             self::$_memoryManager = Zend_Memory::factory('none');
0241         }
0242 
0243         return self::$_memoryManager;
0244     }
0245 
0246     /**
0247      * Set user defined memory manager
0248      *
0249      * @param Zend_Memory_Manager $memoryManager
0250      */
0251     static public function setMemoryManager(Zend_Memory_Manager $memoryManager)
0252     {
0253         self::$_memoryManager = $memoryManager;
0254     }
0255 
0256 
0257     /**
0258      * Create new PDF document from a $source string
0259      *
0260      * @param string $source
0261      * @param integer $revision
0262      * @return Zend_Pdf
0263      */
0264     public static function parse(&$source = null, $revision = null)
0265     {
0266         return new Zend_Pdf($source, $revision);
0267     }
0268 
0269     /**
0270      * Load PDF document from a file
0271      *
0272      * @param string $source
0273      * @param integer $revision
0274      * @return Zend_Pdf
0275      */
0276     public static function load($source = null, $revision = null)
0277     {
0278         return new Zend_Pdf($source, $revision, true);
0279     }
0280 
0281     /**
0282      * Render PDF document and save it.
0283      *
0284      * If $updateOnly is true and it's not a new document, then it only
0285      * appends new section to the end of file.
0286      *
0287      * @param string $filename
0288      * @param boolean $updateOnly
0289      * @throws Zend_Pdf_Exception
0290      */
0291     public function save($filename, $updateOnly = false)
0292     {
0293         if (($file = @fopen($filename, $updateOnly ? 'ab':'wb')) === false ) {
0294             // require_once 'Zend/Pdf/Exception.php';
0295             throw new Zend_Pdf_Exception( "Can not open '$filename' file for writing." );
0296         }
0297 
0298         $this->render($updateOnly, $file);
0299 
0300         fclose($file);
0301     }
0302 
0303     /**
0304      * Creates or loads PDF document.
0305      *
0306      * If $source is null, then it creates a new document.
0307      *
0308      * If $source is a string and $load is false, then it loads document
0309      * from a binary string.
0310      *
0311      * If $source is a string and $load is true, then it loads document
0312      * from a file.
0313      * $revision used to roll back document to specified version
0314      * (0 - current version, 1 - previous version, 2 - ...)
0315      *
0316      * @param string  $source - PDF file to load
0317      * @param integer $revision
0318      * @param bool    $load
0319      * @throws Zend_Pdf_Exception
0320      * @return Zend_Pdf
0321      */
0322     public function __construct($source = null, $revision = null, $load = false)
0323     {
0324         // require_once 'Zend/Pdf/ElementFactory.php';
0325         $this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1);
0326 
0327         if ($source !== null) {
0328             // require_once 'Zend/Pdf/Parser.php';
0329             $this->_parser           = new Zend_Pdf_Parser($source, $this->_objFactory, $load);
0330             $this->_pdfHeaderVersion = $this->_parser->getPDFVersion();
0331             $this->_trailer          = $this->_parser->getTrailer();
0332             if ($this->_trailer->Encrypt !== null) {
0333                 // require_once 'Zend/Pdf/Exception.php';
0334                 throw new Zend_Pdf_Exception('Encrypted document modification is not supported');
0335             }
0336             if ($revision !== null) {
0337                 $this->rollback($revision);
0338             } else {
0339                 $this->_loadPages($this->_trailer->Root->Pages);
0340             }
0341 
0342             $this->_loadNamedDestinations($this->_trailer->Root, $this->_parser->getPDFVersion());
0343             $this->_loadOutlines($this->_trailer->Root);
0344             $this->_loadJavaScript($this->_trailer->Root);
0345             $this->_loadFormFields($this->_trailer->Root);
0346 
0347             if ($this->_trailer->Info !== null) {
0348                 $this->properties = $this->_trailer->Info->toPhp();
0349 
0350                 if (isset($this->properties['Trapped'])) {
0351                     switch ($this->properties['Trapped']) {
0352                         case 'True':
0353                             $this->properties['Trapped'] = true;
0354                             break;
0355 
0356                         case 'False':
0357                             $this->properties['Trapped'] = false;
0358                             break;
0359 
0360                         case 'Unknown':
0361                             $this->properties['Trapped'] = null;
0362                             break;
0363 
0364                         default:
0365                             // Wrong property value
0366                             // Do nothing
0367                             break;
0368                     }
0369                 }
0370 
0371                 $this->_originalProperties = $this->properties;
0372             }
0373 
0374             $this->_isNewDocument = false;
0375         } else {
0376             $this->_pdfHeaderVersion = Zend_Pdf::PDF_VERSION;
0377 
0378             $trailerDictionary = new Zend_Pdf_Element_Dictionary();
0379 
0380             /**
0381              * Document id
0382              */
0383             $docId = md5(uniqid(rand(), true));   // 32 byte (128 bit) identifier
0384             $docIdLow  = substr($docId,  0, 16);  // first 16 bytes
0385             $docIdHigh = substr($docId, 16, 16);  // second 16 bytes
0386 
0387             $trailerDictionary->ID = new Zend_Pdf_Element_Array();
0388             $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdLow);
0389             $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdHigh);
0390 
0391             $trailerDictionary->Size = new Zend_Pdf_Element_Numeric(0);
0392 
0393             // require_once 'Zend/Pdf/Trailer/Generator.php';
0394             $this->_trailer = new Zend_Pdf_Trailer_Generator($trailerDictionary);
0395 
0396             /**
0397              * Document catalog indirect object.
0398              */
0399             $docCatalog = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
0400             $docCatalog->Type    = new Zend_Pdf_Element_Name('Catalog');
0401             $docCatalog->Version = new Zend_Pdf_Element_Name(Zend_Pdf::PDF_VERSION);
0402             $this->_trailer->Root = $docCatalog;
0403 
0404             /**
0405              * Pages container
0406              */
0407             $docPages = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
0408             $docPages->Type  = new Zend_Pdf_Element_Name('Pages');
0409             $docPages->Kids  = new Zend_Pdf_Element_Array();
0410             $docPages->Count = new Zend_Pdf_Element_Numeric(0);
0411             $docCatalog->Pages = $docPages;
0412         }
0413     }
0414 
0415     /**
0416      * Retrive number of revisions.
0417      *
0418      * @return integer
0419      */
0420     public function revisions()
0421     {
0422         $revisions = 1;
0423         $currentTrailer = $this->_trailer;
0424 
0425         while ($currentTrailer->getPrev() !== null && $currentTrailer->getPrev()->Root !== null ) {
0426             $revisions++;
0427             $currentTrailer = $currentTrailer->getPrev();
0428         }
0429 
0430         return $revisions++;
0431     }
0432 
0433     /**
0434      * Rollback document $steps number of revisions.
0435      * This method must be invoked before any changes, applied to the document.
0436      * Otherwise behavior is undefined.
0437      *
0438      * @param integer $steps
0439      */
0440     public function rollback($steps)
0441     {
0442         for ($count = 0; $count < $steps; $count++) {
0443             if ($this->_trailer->getPrev() !== null && $this->_trailer->getPrev()->Root !== null) {
0444                 $this->_trailer = $this->_trailer->getPrev();
0445             } else {
0446                 break;
0447             }
0448         }
0449         $this->_objFactory->setObjectCount($this->_trailer->Size->value);
0450 
0451         // Mark content as modified to force new trailer generation at render time
0452         $this->_trailer->Root->touch();
0453 
0454         $this->pages = array();
0455         $this->_loadPages($this->_trailer->Root->Pages);
0456     }
0457 
0458     /**
0459      * Load pages recursively
0460      *
0461      * @param Zend_Pdf_Element_Reference $pages
0462      * @param array|null                 $attributes
0463      * @throws Zend_Pdf_Exception
0464      */
0465     protected function _loadPages(Zend_Pdf_Element_Reference $pages, $attributes = array())
0466     {
0467         if ($pages->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
0468             // require_once 'Zend/Pdf/Exception.php';
0469             throw new Zend_Pdf_Exception('Wrong argument');
0470         }
0471 
0472         foreach ($pages->getKeys() as $property) {
0473             if (in_array($property, self::$_inheritableAttributes)) {
0474                 $attributes[$property] = $pages->$property;
0475                 $pages->$property = null;
0476             }
0477         }
0478 
0479 
0480         foreach ($pages->Kids->items as $child) {
0481             if ($child->Type->value == 'Pages') {
0482                 $this->_loadPages($child, $attributes);
0483             } else if ($child->Type->value == 'Page') {
0484                 foreach (self::$_inheritableAttributes as $property) {
0485                     if ($child->$property === null && array_key_exists($property, $attributes)) {
0486                         /**
0487                          * Important note.
0488                          * If any attribute or dependant object is an indirect object, then it's still
0489                          * shared between pages.
0490                          */
0491                         if ($attributes[$property] instanceof Zend_Pdf_Element_Object  ||
0492                             $attributes[$property] instanceof Zend_Pdf_Element_Reference) {
0493                             $child->$property = $attributes[$property];
0494                         } else {
0495                             $child->$property = $this->_objFactory->newObject($attributes[$property]);
0496                         }
0497                     }
0498                 }
0499 
0500                 // require_once 'Zend/Pdf/Page.php';
0501                 $this->pages[] = new Zend_Pdf_Page($child, $this->_objFactory);
0502             }
0503         }
0504     }
0505 
0506     /**
0507      * Load named destinations recursively
0508      *
0509      * @param Zend_Pdf_Element_Reference $root Document catalog entry
0510      * @param string $pdfHeaderVersion
0511      * @throws Zend_Pdf_Exception
0512      */
0513     protected function _loadNamedDestinations(Zend_Pdf_Element_Reference $root, $pdfHeaderVersion)
0514     {
0515         if ($root->Version !== null  &&  version_compare($root->Version->value, $pdfHeaderVersion, '>')) {
0516             $versionIs_1_2_plus = version_compare($root->Version->value,    '1.1', '>');
0517         } else {
0518             $versionIs_1_2_plus = version_compare($pdfHeaderVersion, '1.1', '>');
0519         }
0520 
0521         if ($versionIs_1_2_plus) {
0522             // PDF version is 1.2+
0523             // Look for Destinations structure at Name dictionary
0524             if ($root->Names !== null  &&  $root->Names->Dests !== null) {
0525                 // require_once 'Zend/Pdf/NameTree.php';
0526                 // require_once 'Zend/Pdf/Target.php';
0527                 foreach (new Zend_Pdf_NameTree($root->Names->Dests) as $name => $destination) {
0528                     $this->_namedTargets[$name] = Zend_Pdf_Target::load($destination);
0529                 }
0530             }
0531         } else {
0532             // PDF version is 1.1 (or earlier)
0533             // Look for Destinations sructure at Dest entry of document catalog
0534             if ($root->Dests !== null) {
0535                 if ($root->Dests->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
0536                     // require_once 'Zend/Pdf/Exception.php';
0537                     throw new Zend_Pdf_Exception('Document catalog Dests entry must be a dictionary.');
0538                 }
0539 
0540                 // require_once 'Zend/Pdf/Target.php';
0541                 foreach ($root->Dests->getKeys() as $destKey) {
0542                     $this->_namedTargets[$destKey] = Zend_Pdf_Target::load($root->Dests->$destKey);
0543                 }
0544             }
0545         }
0546     }
0547 
0548     /**
0549      * Load outlines recursively
0550      *
0551      * @param Zend_Pdf_Element_Reference $root Document catalog entry
0552      * @throws Zend_Pdf_Exception
0553      */
0554     protected function _loadOutlines(Zend_Pdf_Element_Reference $root)
0555     {
0556         if ($root->Outlines === null) {
0557             return;
0558         }
0559 
0560         if ($root->Outlines->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
0561             // require_once 'Zend/Pdf/Exception.php';
0562             throw new Zend_Pdf_Exception('Document catalog Outlines entry must be a dictionary.');
0563         }
0564 
0565         if ($root->Outlines->Type !== null  &&  $root->Outlines->Type->value != 'Outlines') {
0566             // require_once 'Zend/Pdf/Exception.php';
0567             throw new Zend_Pdf_Exception('Outlines Type entry must be an \'Outlines\' string.');
0568         }
0569 
0570         if ($root->Outlines->First === null) {
0571             return;
0572         }
0573 
0574         $outlineDictionary = $root->Outlines->First;
0575         $processedDictionaries = new SplObjectStorage();
0576         while ($outlineDictionary !== null  &&  !$processedDictionaries->contains($outlineDictionary)) {
0577             $processedDictionaries->attach($outlineDictionary);
0578 
0579             // require_once 'Zend/Pdf/Outline/Loaded.php';
0580             $this->outlines[] = new Zend_Pdf_Outline_Loaded($outlineDictionary);
0581 
0582             $outlineDictionary = $outlineDictionary->Next;
0583         }
0584 
0585         $this->_originalOutlines = $this->outlines;
0586 
0587         if ($root->Outlines->Count !== null) {
0588             $this->_originalOpenOutlinesCount = $root->Outlines->Count->value;
0589         }
0590     }
0591 
0592     /**
0593      * Load JavaScript
0594      *
0595      * Populates the _javaScript string, for later use of getJavaScript method.
0596      *
0597      * @param Zend_Pdf_Element_Reference $root Document catalog entry
0598      */
0599     protected function _loadJavaScript(Zend_Pdf_Element_Reference $root)
0600     {
0601         if (null === $root->Names || null === $root->Names->JavaScript
0602             || null === $root->Names->JavaScript->Names
0603         ) {
0604             return;
0605         }
0606 
0607         foreach ($root->Names->JavaScript->Names->items as $item) {
0608             if ($item instanceof Zend_Pdf_Element_Reference
0609                 && $item->S->value === 'JavaScript'
0610             ) {
0611                 $this->_javaScript[] = $item->JS->value;
0612             }
0613         }
0614     }
0615   
0616     /**
0617      * Load form fields
0618      *
0619      * Populates the _formFields array, for later lookup of fields by name
0620      *
0621      * @param Zend_Pdf_Element_Reference $root Document catalog entry
0622      */
0623     protected function _loadFormFields(Zend_Pdf_Element_Reference $root)
0624     {
0625         if ($root->AcroForm === null || $root->AcroForm->Fields === null) {
0626             return;
0627         }
0628 
0629         foreach ($root->AcroForm->Fields->items as $field) {
0630             /* We only support fields that are textfields and have a name */
0631             if ($field->FT && $field->FT->value == 'Tx' && $field->T
0632                 && $field->T !== null
0633             ) {
0634                 $this->_formFields[$field->T->value] = $field;
0635             }
0636         }
0637 
0638         if (!$root->AcroForm->NeedAppearances
0639             || !$root->AcroForm->NeedAppearances->value
0640         ) {
0641             /* Ask the .pdf viewer to generate its own appearance data, so we do not have to */
0642             $root->AcroForm->add(
0643                 new Zend_Pdf_Element_Name('NeedAppearances'),
0644                 new Zend_Pdf_Element_Boolean(true)
0645             );
0646             $root->AcroForm->touch();
0647         }
0648     }
0649 
0650     /**
0651      * Retrieves a list with the names of the AcroForm textfields in the PDF
0652      *
0653      * @return array of strings
0654      */
0655     public function getTextFieldNames()
0656     {
0657         return array_keys($this->_formFields);
0658     }
0659 
0660     /**
0661      * Sets the value of an AcroForm text field
0662      *
0663      * @param string $name Name of textfield
0664      * @param string $value Value
0665      * @throws Zend_Pdf_Exception if the textfield does not exist in the pdf
0666      */
0667     public function setTextField($name, $value)
0668     {
0669         if (!isset($this->_formFields[$name])) {
0670             throw new Zend_Pdf_Exception(
0671                 "Field '$name' does not exist or is not a textfield"
0672             );
0673         }
0674 
0675         /** @var Zend_Pdf_Element $field */
0676         $field = $this->_formFields[$name];
0677         $field->add(
0678             new Zend_Pdf_Element_Name('V'), new Zend_Pdf_Element_String($value)
0679         );
0680         $field->touch();
0681     }
0682 
0683     /**
0684      * Sets the properties for an AcroForm text field
0685      *
0686      * @param string $name
0687      * @param mixed  $bitmask
0688      * @throws Zend_Pdf_Exception
0689      */
0690     public function setTextFieldProperties($name, $bitmask)
0691     {
0692         if (!isset($this->_formFields[$name])) {
0693             throw new Zend_Pdf_Exception(
0694                 "Field '$name' does not exist or is not a textfield"
0695             );
0696         }
0697 
0698         $field = $this->_formFields[$name];
0699         $field->add(
0700             new Zend_Pdf_Element_Name('Ff'),
0701             new Zend_Pdf_Element_Numeric($bitmask)
0702         );
0703         $field->touch();
0704     }
0705 
0706     /**
0707      * Marks an AcroForm text field as read only
0708      *
0709      * @param string $name
0710      */
0711     public function markTextFieldAsReadOnly($name)
0712     {
0713         $this->setTextFieldProperties($name, self::PDF_FORM_FIELD_READONLY);
0714     }
0715 
0716     /**
0717      * Orginize pages to tha pages tree structure.
0718      *
0719      * @todo atomatically attach page to the document, if it's not done yet.
0720      * @todo check, that page is attached to the current document
0721      *
0722      * @todo Dump pages as a balanced tree instead of a plain set.
0723      */
0724     protected function _dumpPages()
0725     {
0726         $root = $this->_trailer->Root;
0727         $pagesContainer = $root->Pages;
0728 
0729         $pagesContainer->touch();
0730         $pagesContainer->Kids->items = array();
0731 
0732         foreach ($this->pages as $page ) {
0733             $page->render($this->_objFactory);
0734 
0735             $pageDictionary = $page->getPageDictionary();
0736             $pageDictionary->touch();
0737             $pageDictionary->Parent = $pagesContainer;
0738 
0739             $pagesContainer->Kids->items[] = $pageDictionary;
0740         }
0741 
0742         $this->_refreshPagesHash();
0743 
0744         $pagesContainer->Count->touch();
0745         $pagesContainer->Count->value = count($this->pages);
0746 
0747 
0748         // Refresh named destinations list
0749         foreach ($this->_namedTargets as $name => $namedTarget) {
0750             if ($namedTarget instanceof Zend_Pdf_Destination_Explicit) {
0751                 // Named target is an explicit destination
0752                 if ($this->resolveDestination($namedTarget, false) === null) {
0753                     unset($this->_namedTargets[$name]);
0754                 }
0755             } else if ($namedTarget instanceof Zend_Pdf_Action) {
0756                 // Named target is an action
0757                 if ($this->_cleanUpAction($namedTarget, false) === null) {
0758                     // Action is a GoTo action with an unresolved destination
0759                     unset($this->_namedTargets[$name]);
0760                 }
0761             } else {
0762                 // require_once 'Zend/Pdf/Exception.php';
0763                 throw new Zend_Pdf_Exception('Wrong type of named targed (\'' . get_class($namedTarget) . '\').');
0764             }
0765         }
0766 
0767         // Refresh outlines
0768         // require_once 'Zend/Pdf/RecursivelyIteratableObjectsContainer.php';
0769         $iterator = new RecursiveIteratorIterator(new Zend_Pdf_RecursivelyIteratableObjectsContainer($this->outlines), RecursiveIteratorIterator::SELF_FIRST);
0770         foreach ($iterator as $outline) {
0771             $target = $outline->getTarget();
0772 
0773             if ($target !== null) {
0774                 if ($target instanceof Zend_Pdf_Destination) {
0775                     // Outline target is a destination
0776                     if ($this->resolveDestination($target, false) === null) {
0777                         $outline->setTarget(null);
0778                     }
0779                 } else if ($target instanceof Zend_Pdf_Action) {
0780                     // Outline target is an action
0781                     if ($this->_cleanUpAction($target, false) === null) {
0782                         // Action is a GoTo action with an unresolved destination
0783                         $outline->setTarget(null);
0784                     }
0785                 } else {
0786                     // require_once 'Zend/Pdf/Exception.php';
0787                     throw new Zend_Pdf_Exception('Wrong outline target.');
0788                 }
0789             }
0790         }
0791 
0792         $openAction = $this->getOpenAction();
0793         if ($openAction !== null) {
0794             if ($openAction instanceof Zend_Pdf_Action) {
0795                 // OpenAction is an action
0796                 if ($this->_cleanUpAction($openAction, false) === null) {
0797                     // Action is a GoTo action with an unresolved destination
0798                     $this->setOpenAction(null);
0799                 }
0800             } else if ($openAction instanceof Zend_Pdf_Destination) {
0801                 // OpenAction target is a destination
0802                 if ($this->resolveDestination($openAction, false) === null) {
0803                     $this->setOpenAction(null);
0804                 }
0805             } else {
0806                 // require_once 'Zend/Pdf/Exception.php';
0807                 throw new Zend_Pdf_Exception('OpenAction has to be either PDF Action or Destination.');
0808             }
0809         }
0810     }
0811 
0812     /**
0813      * Dump named destinations
0814      *
0815      * @todo Create a balanced tree instead of plain structure.
0816      */
0817     protected function _dumpNamedDestinations()
0818     {
0819         ksort($this->_namedTargets, SORT_STRING);
0820 
0821         $destArrayItems = array();
0822         foreach ($this->_namedTargets as $name => $destination) {
0823             $destArrayItems[] = new Zend_Pdf_Element_String($name);
0824 
0825             if ($destination instanceof Zend_Pdf_Target) {
0826                 $destArrayItems[] = $destination->getResource();
0827             } else {
0828                 // require_once 'Zend/Pdf/Exception.php';
0829                 throw new Zend_Pdf_Exception('PDF named destinations must be a Zend_Pdf_Target object.');
0830             }
0831         }
0832         $destArray = $this->_objFactory->newObject(new Zend_Pdf_Element_Array($destArrayItems));
0833 
0834         $DestTree = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
0835         $DestTree->Names = $destArray;
0836 
0837         $root = $this->_trailer->Root;
0838 
0839         if ($root->Names === null) {
0840             $root->touch();
0841             $root->Names = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
0842         } else {
0843             $root->Names->touch();
0844         }
0845         $root->Names->Dests = $DestTree;
0846     }
0847 
0848     /**
0849      * Dump outlines recursively
0850      */
0851     protected function _dumpOutlines()
0852     {
0853         $root = $this->_trailer->Root;
0854 
0855         if ($root->Outlines === null) {
0856             if (count($this->outlines) == 0) {
0857                 return;
0858             } else {
0859                 $root->Outlines = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
0860                 $root->Outlines->Type = new Zend_Pdf_Element_Name('Outlines');
0861                 $updateOutlinesNavigation = true;
0862             }
0863         } else {
0864             $updateOutlinesNavigation = false;
0865             if (count($this->_originalOutlines) != count($this->outlines)) {
0866                 // If original and current outlines arrays have different size then outlines list was updated
0867                 $updateOutlinesNavigation = true;
0868             } else if ( !(array_keys($this->_originalOutlines) === array_keys($this->outlines)) ) {
0869                 // If original and current outlines arrays have different keys (with a glance to an order) then outlines list was updated
0870                 $updateOutlinesNavigation = true;
0871             } else {
0872                 foreach ($this->outlines as $key => $outline) {
0873                     if ($this->_originalOutlines[$key] !== $outline) {
0874                         $updateOutlinesNavigation = true;
0875                     }
0876                 }
0877             }
0878         }
0879 
0880         $lastOutline = null;
0881         $openOutlinesCount = 0;
0882         if ($updateOutlinesNavigation) {
0883             $root->Outlines->touch();
0884             $root->Outlines->First = null;
0885 
0886             foreach ($this->outlines as $outline) {
0887                 if ($lastOutline === null) {
0888                     // First pass. Update Outlines dictionary First entry using corresponding value
0889                     $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines);
0890                     $root->Outlines->First = $lastOutline;
0891                 } else {
0892                     // Update previous outline dictionary Next entry (Prev is updated within dumpOutline() method)
0893                     $currentOutlineDictionary = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline);
0894                     $lastOutline->Next = $currentOutlineDictionary;
0895                     $lastOutline       = $currentOutlineDictionary;
0896                 }
0897                 $openOutlinesCount += $outline->openOutlinesCount();
0898             }
0899 
0900             $root->Outlines->Last  = $lastOutline;
0901         } else {
0902             foreach ($this->outlines as $outline) {
0903                 $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline);
0904                 $openOutlinesCount += $outline->openOutlinesCount();
0905             }
0906         }
0907 
0908         if ($openOutlinesCount != $this->_originalOpenOutlinesCount) {
0909             $root->Outlines->touch;
0910             $root->Outlines->Count = new Zend_Pdf_Element_Numeric($openOutlinesCount);
0911         }
0912     }
0913 
0914     /**
0915      * Create page object, attached to the PDF document.
0916      * Method signatures:
0917      *
0918      * 1. Create new page with a specified pagesize.
0919      *    If $factory is null then it will be created and page must be attached to the document to be
0920      *    included into output.
0921      * ---------------------------------------------------------
0922      * new Zend_Pdf_Page(string $pagesize);
0923      * ---------------------------------------------------------
0924      *
0925      * 2. Create new page with a specified pagesize (in default user space units).
0926      *    If $factory is null then it will be created and page must be attached to the document to be
0927      *    included into output.
0928      * ---------------------------------------------------------
0929      * new Zend_Pdf_Page(numeric $width, numeric $height);
0930      * ---------------------------------------------------------
0931      *
0932      * @param mixed $param1
0933      * @param mixed $param2
0934      * @return Zend_Pdf_Page
0935      */
0936     public function newPage($param1, $param2 = null)
0937     {
0938         // require_once 'Zend/Pdf/Page.php';
0939         if ($param2 === null) {
0940             return new Zend_Pdf_Page($param1, $this->_objFactory);
0941         } else {
0942             return new Zend_Pdf_Page($param1, $param2, $this->_objFactory);
0943         }
0944     }
0945 
0946     /**
0947      * Return the document-level Metadata
0948      * or null Metadata stream is not presented
0949      *
0950      * @return string
0951      */
0952     public function getMetadata()
0953     {
0954         if ($this->_trailer->Root->Metadata !== null) {
0955             return $this->_trailer->Root->Metadata->value;
0956         } else {
0957             return null;
0958         }
0959     }
0960 
0961     /**
0962      * Sets the document-level Metadata (mast be valid XMP document)
0963      *
0964      * @param string $metadata
0965      */
0966     public function setMetadata($metadata)
0967     {
0968         $metadataObject = $this->_objFactory->newStreamObject($metadata);
0969         $metadataObject->dictionary->Type    = new Zend_Pdf_Element_Name('Metadata');
0970         $metadataObject->dictionary->Subtype = new Zend_Pdf_Element_Name('XML');
0971 
0972         $this->_trailer->Root->Metadata = $metadataObject;
0973         $this->_trailer->Root->touch();
0974     }
0975 
0976     /**
0977      * Return the document-level JavaScript
0978      * or null if there is no JavaScript for this document
0979      *
0980      * @return string
0981      */
0982     public function getJavaScript()
0983     {
0984         return $this->_javaScript;
0985     }
0986 
0987     /**
0988      * Get open Action
0989      * Returns Zend_Pdf_Target (Zend_Pdf_Destination or Zend_Pdf_Action object)
0990      *
0991      * @return Zend_Pdf_Target
0992      */
0993     public function getOpenAction()
0994     {
0995         if ($this->_trailer->Root->OpenAction !== null) {
0996             // require_once 'Zend/Pdf/Target.php';
0997             return Zend_Pdf_Target::load($this->_trailer->Root->OpenAction);
0998         } else {
0999             return null;
1000         }
1001     }
1002 
1003     /**
1004      * Set open Action which is actually Zend_Pdf_Destination or Zend_Pdf_Action object
1005      *
1006      * @param Zend_Pdf_Target $openAction
1007      * @returns Zend_Pdf
1008      */
1009     public function setOpenAction(Zend_Pdf_Target $openAction = null)
1010     {
1011         $root = $this->_trailer->Root;
1012         $root->touch();
1013 
1014         if ($openAction === null) {
1015             $root->OpenAction = null;
1016         } else {
1017             $root->OpenAction = $openAction->getResource();
1018 
1019             if ($openAction instanceof Zend_Pdf_Action)  {
1020                 $openAction->dumpAction($this->_objFactory);
1021             }
1022         }
1023 
1024         return $this;
1025     }
1026 
1027     /**
1028      * Return an associative array containing all the named destinations (or GoTo actions) in the PDF.
1029      * Named targets can be used to reference from outside
1030      * the PDF, ex: 'http://www.something.com/mydocument.pdf#MyAction'
1031      *
1032      * @return array
1033      */
1034     public function getNamedDestinations()
1035     {
1036         return $this->_namedTargets;
1037     }
1038 
1039     /**
1040      * Return specified named destination
1041      *
1042      * @param string $name
1043      * @return Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo
1044      */
1045     public function getNamedDestination($name)
1046     {
1047         if (isset($this->_namedTargets[$name])) {
1048             return $this->_namedTargets[$name];
1049         } else {
1050             return null;
1051         }
1052     }
1053 
1054     /**
1055      * Set specified named destination
1056      *
1057      * @param string                                             $name
1058      * @param Zend_Pdf_Destination_Explicit|Zend_Pdf_Action_GoTo $destination
1059      * @throws Zend_Pdf_Exception
1060      */
1061     public function setNamedDestination($name, $destination = null)
1062     {
1063         if ($destination !== null  &&
1064             !$destination instanceof Zend_Pdf_Action_GoTo  &&
1065             !$destination instanceof Zend_Pdf_Destination_Explicit) {
1066             // require_once 'Zend/Pdf/Exception.php';
1067             throw new Zend_Pdf_Exception('PDF named destination must refer an explicit destination or a GoTo PDF action.');
1068         }
1069 
1070         if ($destination !== null) {
1071            $this->_namedTargets[$name] = $destination;
1072         } else {
1073             unset($this->_namedTargets[$name]);
1074         }
1075     }
1076 
1077     /**
1078      * Pages collection hash:
1079      * <page dictionary object hash id> => Zend_Pdf_Page
1080      *
1081      * @var SplObjectStorage
1082      */
1083     protected $_pageReferences = null;
1084 
1085     /**
1086      * Pages collection hash:
1087      * <page number> => Zend_Pdf_Page
1088      *
1089      * @var array
1090      */
1091     protected $_pageNumbers = null;
1092 
1093     /**
1094      * Refresh page collection hashes
1095      *
1096      * @return Zend_Pdf
1097      */
1098     protected function _refreshPagesHash()
1099     {
1100         $this->_pageReferences = array();
1101         $this->_pageNumbers    = array();
1102         $count = 1;
1103         foreach ($this->pages as $page) {
1104             $pageDictionaryHashId = spl_object_hash($page->getPageDictionary()->getObject());
1105             $this->_pageReferences[$pageDictionaryHashId] = $page;
1106             $this->_pageNumbers[$count++]                 = $page;
1107         }
1108 
1109         return $this;
1110     }
1111 
1112     /**
1113      * Resolve destination.
1114      *
1115      * Returns Zend_Pdf_Page page object or null if destination is not found within PDF document.
1116      *
1117      * @param Zend_Pdf_Destination $destination Destination to resolve
1118      * @param bool $refreshPageCollectionHashes Refresh page collection hashes before processing
1119      * @return Zend_Pdf_Page|null
1120      * @throws Zend_Pdf_Exception
1121      */
1122     public function resolveDestination(Zend_Pdf_Destination $destination, $refreshPageCollectionHashes = true)
1123     {
1124         if ($this->_pageReferences === null  ||  $refreshPageCollectionHashes) {
1125             $this->_refreshPagesHash();
1126         }
1127 
1128         if ($destination instanceof Zend_Pdf_Destination_Named) {
1129             if (!isset($this->_namedTargets[$destination->getName()])) {
1130                 return null;
1131             }
1132             $destination = $this->getNamedDestination($destination->getName());
1133 
1134             if ($destination instanceof Zend_Pdf_Action) {
1135                 if (!$destination instanceof Zend_Pdf_Action_GoTo) {
1136                     return null;
1137                 }
1138                 $destination = $destination->getDestination();
1139             }
1140 
1141             if (!$destination instanceof Zend_Pdf_Destination_Explicit) {
1142                 // require_once 'Zend/Pdf/Exception.php';
1143                 throw new Zend_Pdf_Exception('Named destination target has to be an explicit destination.');
1144             }
1145         }
1146 
1147         // Named target is an explicit destination
1148         $pageElement = $destination->getResource()->items[0];
1149 
1150         if ($pageElement->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
1151             // Page reference is a PDF number
1152             if (!isset($this->_pageNumbers[$pageElement->value])) {
1153                 return null;
1154             }
1155 
1156             return $this->_pageNumbers[$pageElement->value];
1157         }
1158 
1159         // Page reference is a PDF page dictionary reference
1160         $pageDictionaryHashId = spl_object_hash($pageElement->getObject());
1161         if (!isset($this->_pageReferences[$pageDictionaryHashId])) {
1162             return null;
1163         }
1164         return $this->_pageReferences[$pageDictionaryHashId];
1165     }
1166 
1167     /**
1168      * Walk through action and its chained actions tree and remove nodes
1169      * if they are GoTo actions with an unresolved target.
1170      *
1171      * Returns null if root node is deleted or updated action overwise.
1172      *
1173      * @todo Give appropriate name and make method public
1174      *
1175      * @param Zend_Pdf_Action $action
1176      * @param bool $refreshPageCollectionHashes Refresh page collection hashes before processing
1177      * @return Zend_Pdf_Action|null
1178      */
1179     protected function _cleanUpAction(Zend_Pdf_Action $action, $refreshPageCollectionHashes = true)
1180     {
1181         if ($this->_pageReferences === null  ||  $refreshPageCollectionHashes) {
1182             $this->_refreshPagesHash();
1183         }
1184 
1185         // Named target is an action
1186         if ($action instanceof Zend_Pdf_Action_GoTo  &&
1187             $this->resolveDestination($action->getDestination(), false) === null) {
1188             // Action itself is a GoTo action with an unresolved destination
1189             return null;
1190         }
1191 
1192         // Walk through child actions
1193         $iterator = new RecursiveIteratorIterator($action, RecursiveIteratorIterator::SELF_FIRST);
1194 
1195         $actionsToClean        = array();
1196         $deletionCandidateKeys = array();
1197         foreach ($iterator as $chainedAction) {
1198             if ($chainedAction instanceof Zend_Pdf_Action_GoTo  &&
1199                 $this->resolveDestination($chainedAction->getDestination(), false) === null) {
1200                 // Some child action is a GoTo action with an unresolved destination
1201                 // Mark it as a candidate for deletion
1202                 $actionsToClean[]        = $iterator->getSubIterator();
1203                 $deletionCandidateKeys[] = $iterator->getSubIterator()->key();
1204             }
1205         }
1206         foreach ($actionsToClean as $id => $action) {
1207             unset($action->next[$deletionCandidateKeys[$id]]);
1208         }
1209 
1210         return $action;
1211     }
1212 
1213     /**
1214      * Extract fonts attached to the document
1215      *
1216      * returns array of Zend_Pdf_Resource_Font_Extracted objects
1217      *
1218      * @return array
1219      * @throws Zend_Pdf_Exception
1220      */
1221     public function extractFonts()
1222     {
1223         $fontResourcesUnique = array();
1224         foreach ($this->pages as $page) {
1225             $pageResources = $page->extractResources();
1226 
1227             if ($pageResources->Font === null) {
1228                 // Page doesn't contain have any font reference
1229                 continue;
1230             }
1231 
1232             $fontResources = $pageResources->Font;
1233 
1234             foreach ($fontResources->getKeys() as $fontResourceName) {
1235                 $fontDictionary = $fontResources->$fontResourceName;
1236 
1237                 if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference  ||
1238                        $fontDictionary instanceof Zend_Pdf_Element_Object) ) {
1239                     // require_once 'Zend/Pdf/Exception.php';
1240                     throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.');
1241                 }
1242 
1243                 $fontResourcesUnique[spl_object_hash($fontDictionary->getObject())] = $fontDictionary;
1244             }
1245         }
1246 
1247         $fonts = array();
1248         // require_once 'Zend/Pdf/Exception.php';
1249         foreach ($fontResourcesUnique as $resourceId => $fontDictionary) {
1250             try {
1251                 // Try to extract font
1252                 // require_once 'Zend/Pdf/Resource/Font/Extracted.php';
1253                 $extractedFont = new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
1254 
1255                 $fonts[$resourceId] = $extractedFont;
1256             } catch (Zend_Pdf_Exception $e) {
1257                 if ($e->getMessage() != 'Unsupported font type.') {
1258                     throw $e;
1259                 }
1260             }
1261         }
1262 
1263         return $fonts;
1264     }
1265 
1266     /**
1267      * Extract font attached to the page by specific font name
1268      *
1269      * $fontName should be specified in UTF-8 encoding
1270      *
1271      * @param string $fontName
1272      * @return Zend_Pdf_Resource_Font_Extracted|null
1273      * @throws Zend_Pdf_Exception
1274      */
1275     public function extractFont($fontName)
1276     {
1277         $fontResourcesUnique = array();
1278         // require_once 'Zend/Pdf/Exception.php';
1279         foreach ($this->pages as $page) {
1280             $pageResources = $page->extractResources();
1281 
1282             if ($pageResources->Font === null) {
1283                 // Page doesn't contain have any font reference
1284                 continue;
1285             }
1286 
1287             $fontResources = $pageResources->Font;
1288 
1289             foreach ($fontResources->getKeys() as $fontResourceName) {
1290                 $fontDictionary = $fontResources->$fontResourceName;
1291 
1292                 if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference  ||
1293                        $fontDictionary instanceof Zend_Pdf_Element_Object) ) {
1294                     // require_once 'Zend/Pdf/Exception.php';
1295                     throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.');
1296                 }
1297 
1298                 $resourceId = spl_object_hash($fontDictionary->getObject());
1299                 if (isset($fontResourcesUnique[$resourceId])) {
1300                     continue;
1301                 } else {
1302                     // Mark resource as processed
1303                     $fontResourcesUnique[$resourceId] = 1;
1304                 }
1305 
1306                 if ($fontDictionary->BaseFont->value != $fontName) {
1307                     continue;
1308                 }
1309 
1310                 try {
1311                     // Try to extract font
1312                     // require_once 'Zend/Pdf/Resource/Font/Extracted.php';
1313                     return new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
1314                 } catch (Zend_Pdf_Exception $e) {
1315                     if ($e->getMessage() != 'Unsupported font type.') {
1316                         throw $e;
1317                     }
1318                     // Continue searhing
1319                 }
1320             }
1321         }
1322 
1323         return null;
1324     }
1325 
1326     /**
1327      * Render the completed PDF to a string.
1328      * If $newSegmentOnly is true and it's not a new document,
1329      * then only appended part of PDF is returned.
1330      *
1331      * @param boolean $newSegmentOnly
1332      * @param resource $outputStream
1333      * @return string
1334      * @throws Zend_Pdf_Exception
1335      */
1336     public function render($newSegmentOnly = false, $outputStream = null)
1337     {
1338         if ($this->_isNewDocument) {
1339             // Drop full document first time even $newSegmentOnly is set to true
1340             $newSegmentOnly = false;
1341             $this->_isNewDocument = false;
1342         }
1343 
1344         // Save document properties if necessary
1345         if ($this->properties != $this->_originalProperties) {
1346             $docInfo = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
1347 
1348             foreach ($this->properties as $key => $value) {
1349                 switch ($key) {
1350                     case 'Trapped':
1351                         switch ($value) {
1352                             case true:
1353                                 $docInfo->$key = new Zend_Pdf_Element_Name('True');
1354                                 break;
1355 
1356                             case false:
1357                                 $docInfo->$key = new Zend_Pdf_Element_Name('False');
1358                                 break;
1359 
1360                             case null:
1361                                 $docInfo->$key = new Zend_Pdf_Element_Name('Unknown');
1362                                 break;
1363 
1364                             default:
1365                                 // require_once 'Zend/Pdf/Exception.php';
1366                                 throw new Zend_Pdf_Exception('Wrong Trapped document property vale: \'' . $value . '\'. Only true, false and null values are allowed.');
1367                                 break;
1368                         }
1369 
1370                     case 'CreationDate':
1371                         // break intentionally omitted
1372                     case 'ModDate':
1373                         $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
1374                         break;
1375 
1376                     case 'Title':
1377                         // break intentionally omitted
1378                     case 'Author':
1379                         // break intentionally omitted
1380                     case 'Subject':
1381                         // break intentionally omitted
1382                     case 'Keywords':
1383                         // break intentionally omitted
1384                     case 'Creator':
1385                         // break intentionally omitted
1386                     case 'Producer':
1387                         if (extension_loaded('mbstring') === true) {
1388                             $detected = mb_detect_encoding($value);
1389                             if ($detected !== 'ASCII') {
1390                                 $value = "\xfe\xff" . mb_convert_encoding($value, 'UTF-16', $detected);
1391                             }
1392                         }
1393                         $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
1394                         break;
1395 
1396                     default:
1397                         // Set property using PDF type based on PHP type
1398                         $docInfo->$key = Zend_Pdf_Element::phpToPdf($value);
1399                         break;
1400                 }
1401             }
1402 
1403             $this->_trailer->Info = $docInfo;
1404         }
1405 
1406         $this->_dumpPages();
1407         $this->_dumpNamedDestinations();
1408         $this->_dumpOutlines();
1409 
1410         // Check, that PDF file was modified
1411         // File is always modified by _dumpPages() now, but future implementations may eliminate this.
1412         if (!$this->_objFactory->isModified()) {
1413             if ($newSegmentOnly) {
1414                 // Do nothing, return
1415                 return '';
1416             }
1417 
1418             if ($outputStream === null) {
1419                 return $this->_trailer->getPDFString();
1420             } else {
1421                 $pdfData = $this->_trailer->getPDFString();
1422                 while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) {
1423                     $pdfData = substr($pdfData, $byteCount);
1424                 }
1425 
1426                 return '';
1427             }
1428         }
1429 
1430         // offset (from a start of PDF file) of new PDF file segment
1431         $offset = $this->_trailer->getPDFLength();
1432         // Last Object number in a list of free objects
1433         $lastFreeObject = $this->_trailer->getLastFreeObject();
1434 
1435         // Array of cross-reference table subsections
1436         $xrefTable = array();
1437         // Object numbers of first objects in each subsection
1438         $xrefSectionStartNums = array();
1439 
1440         // Last cross-reference table subsection
1441         $xrefSection = array();
1442         // Dummy initialization of the first element (specail case - header of linked list of free objects).
1443         $xrefSection[] = 0;
1444         $xrefSectionStartNums[] = 0;
1445         // Object number of last processed PDF object.
1446         // Used to manage cross-reference subsections.
1447         // Initialized by zero (specail case - header of linked list of free objects).
1448         $lastObjNum = 0;
1449 
1450         if ($outputStream !== null) {
1451             if (!$newSegmentOnly) {
1452                 $pdfData = $this->_trailer->getPDFString();
1453                 while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) {
1454                     $pdfData = substr($pdfData, $byteCount);
1455                 }
1456             }
1457         } else {
1458             $pdfSegmentBlocks = ($newSegmentOnly) ? array() : array($this->_trailer->getPDFString());
1459         }
1460 
1461         // Iterate objects to create new reference table
1462         foreach ($this->_objFactory->listModifiedObjects() as $updateInfo) {
1463             $objNum = $updateInfo->getObjNum();
1464 
1465             if ($objNum - $lastObjNum != 1) {
1466                 // Save cross-reference table subsection and start new one
1467                 $xrefTable[] = $xrefSection;
1468                 $xrefSection = array();
1469                 $xrefSectionStartNums[] = $objNum;
1470             }
1471 
1472             if ($updateInfo->isFree()) {
1473                 // Free object cross-reference table entry
1474                 $xrefSection[]  = sprintf("%010d %05d f \n", $lastFreeObject, $updateInfo->getGenNum());
1475                 $lastFreeObject = $objNum;
1476             } else {
1477                 // In-use object cross-reference table entry
1478                 $xrefSection[]  = sprintf("%010d %05d n \n", $offset, $updateInfo->getGenNum());
1479 
1480                 $pdfBlock = $updateInfo->getObjectDump();
1481                 $offset += strlen($pdfBlock);
1482 
1483                 if ($outputStream === null) {
1484                     $pdfSegmentBlocks[] = $pdfBlock;
1485                 } else {
1486                     while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
1487                         $pdfBlock = substr($pdfBlock, $byteCount);
1488                     }
1489                 }
1490             }
1491             $lastObjNum = $objNum;
1492         }
1493         // Save last cross-reference table subsection
1494         $xrefTable[] = $xrefSection;
1495 
1496         // Modify first entry (specail case - header of linked list of free objects).
1497         $xrefTable[0][0] = sprintf("%010d 65535 f \n", $lastFreeObject);
1498 
1499         $xrefTableStr = "xref\n";
1500         foreach ($xrefTable as $sectId => $xrefSection) {
1501             $xrefTableStr .= sprintf("%d %d \n", $xrefSectionStartNums[$sectId], count($xrefSection));
1502             foreach ($xrefSection as $xrefTableEntry) {
1503                 $xrefTableStr .= $xrefTableEntry;
1504             }
1505         }
1506 
1507         $this->_trailer->Size->value = $this->_objFactory->getObjectCount();
1508 
1509         $pdfBlock = $xrefTableStr
1510                  .  $this->_trailer->toString()
1511                  . "startxref\n" . $offset . "\n"
1512                  . "%%EOF\n";
1513 
1514         $this->_objFactory->cleanEnumerationShiftCache();
1515 
1516         if ($outputStream === null) {
1517             $pdfSegmentBlocks[] = $pdfBlock;
1518 
1519             return implode('', $pdfSegmentBlocks);
1520         } else {
1521             while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
1522                 $pdfBlock = substr($pdfBlock, $byteCount);
1523             }
1524 
1525             return '';
1526         }
1527     }
1528 
1529     /**
1530      * Sets the document-level JavaScript
1531      *
1532      * Resets and appends
1533      *
1534      * @param string|array $javaScript
1535      */
1536     public function setJavaScript($javaScript)
1537     {
1538         $this->resetJavaScript();
1539 
1540         $this->addJavaScript($javaScript);
1541     }
1542 
1543     /**
1544      * Resets the document-level JavaScript
1545      */
1546     public function resetJavaScript()
1547     {
1548         $this->_javaScript = null;
1549 
1550         $root = $this->_trailer->Root;
1551         if (null === $root->Names || null === $root->Names->JavaScript) {
1552             return;
1553         }
1554         $root->Names->JavaScript = null;
1555     }
1556 
1557     /**
1558      * Appends JavaScript to the document-level JavaScript
1559      *
1560      * @param string|array $javaScript
1561      * @throws Zend_Pdf_Exception
1562      */
1563     public function addJavaScript($javaScript)
1564     {
1565         if (empty($javaScript)) {
1566             throw new Zend_Pdf_Exception(
1567                 'JavaScript must be a non empty string or array of strings'
1568             );
1569         }
1570 
1571         if (!is_array($javaScript)) {
1572             $javaScript = array($javaScript);
1573         }
1574 
1575         if (null === $this->_javaScript) {
1576             $this->_javaScript = $javaScript;
1577         } else {
1578             $this->_javaScript = array_merge($this->_javaScript, $javaScript);
1579         }
1580 
1581         if (!empty($this->_javaScript)) {
1582             $items = array();
1583 
1584             foreach ($this->_javaScript as $javaScript) {
1585                 $jsCode = array(
1586                     'S'  => new Zend_Pdf_Element_Name('JavaScript'),
1587                     'JS' => new Zend_Pdf_Element_String($javaScript)
1588                 );
1589                 $items[] = new Zend_Pdf_Element_String('EmbeddedJS');
1590                 $items[] = $this->_objFactory->newObject(
1591                     new Zend_Pdf_Element_Dictionary($jsCode)
1592                 );
1593             }
1594 
1595             $jsRef = $this->_objFactory->newObject(
1596                 new Zend_Pdf_Element_Dictionary(
1597                     array('Names' => new Zend_Pdf_Element_Array($items))
1598                 )
1599             );
1600 
1601             if (null === $this->_trailer->Root->Names) {
1602                 $this->_trailer->Root->Names = new Zend_Pdf_Element_Dictionary();
1603             }
1604             $this->_trailer->Root->Names->JavaScript = $jsRef;
1605         }
1606     }
1607 
1608     /**
1609      * Convert date to PDF format (it's close to ASN.1 (Abstract Syntax Notation
1610      * One) defined in ISO/IEC 8824).
1611      *
1612      * @todo This really isn't the best location for this method. It should
1613      *   probably actually exist as Zend_Pdf_Element_Date or something like that.
1614      *
1615      * @todo Address the following E_STRICT issue:
1616      *   PHP Strict Standards:  date(): It is not safe to rely on the system's
1617      *   timezone settings. Please use the date.timezone setting, the TZ
1618      *   environment variable or the date_default_timezone_set() function. In
1619      *   case you used any of those methods and you are still getting this
1620      *   warning, you most likely misspelled the timezone identifier.
1621      *
1622      * @param integer $timestamp (optional) If omitted, uses the current time.
1623      * @return string
1624      */
1625     public static function pdfDate($timestamp = null)
1626     {
1627         if ($timestamp === null) {
1628             $date = date('\D\:YmdHisO');
1629         } else {
1630             $date = date('\D\:YmdHisO', $timestamp);
1631         }
1632         return substr_replace($date, '\'', -2, 0) . '\'';
1633     }
1634 
1635 }