Forráskód Böngészése

Zend_Pdf: ZF-5832, ZF-6911, ZF-6914, ZF-6915 related commit.

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@16978 44c647ce-9c0f-0410-b52a-842ac1e357ba
alexander 16 éve
szülő
commit
d87f19b772

+ 348 - 185
library/Zend/Pdf.php

@@ -79,6 +79,15 @@ require_once 'Zend/Pdf/Action.php';
 /** Zend_Pdf_Destination */
 require_once 'Zend/Pdf/Destination.php';
 
+/** Zend_Pdf_Outline_Created */
+require_once 'Zend/Pdf/Outline/Created.php';
+
+/** Zend_Pdf_Outline_Loaded */
+require_once 'Zend/Pdf/Outline/Loaded.php';
+
+/** Zend_Pdf_Outline_Container */
+require_once 'Zend/Pdf/Outline/Container.php';
+
 /** Zend_Pdf_Destination */
 require_once 'Zend/Pdf/Exception.php';
 
@@ -174,6 +183,29 @@ class Zend_Pdf
     protected $_namedDestinations = array();
 
     /**
+     * Document outlines
+     *
+     * @var array - array of Zend_Pdf_Outline objects
+     */
+    public $outlines = array();
+
+    /**
+     * Original document outlines list
+     * Used to track outlines update
+     *
+     * @var array - array of Zend_Pdf_Outline objects
+     */
+    protected $_originalOutlines = array();
+
+    /**
+     * Original document outlines open elements count
+     * Used to track outlines update
+     *
+     * @var integer
+     */
+    protected $_originalOpenOutlinesCount = 0;
+
+    /**
      * Pdf trailer (last or just created)
      *
      * @var Zend_Pdf_Trailer
@@ -281,28 +313,6 @@ class Zend_Pdf
     }
 
     /**
-     * Parse destination structure (array or dictionary) and
-     * return it as a Zend_Pdf_Destination or Zend_Pdf_Action object
-     *
-     * $param Zend_Pdf_Element $destination
-     * @return Zend_Pdf_Destination|
-     * @throws Zend_Pdf_Exception
-     */
-    protected function _loadDestination(Zend_Pdf_Element $destination) {
-        if ($destination->getType() == Zend_Pdf_Element::TYPE_ARRAY) {
-            // Destination is an array, just treat it as an explicit destination array
-            return Zend_Pdf_Destination::load($destination);
-        } else if ($destination->getType() == Zend_Pdf_Element::TYPE_DICTIONARY) {
-        	// Load destination as appropriate action
-            return Zend_Pdf_Action::load($destination);
-        } else {
-            require_once 'Zend/Pdf/Exception.php';
-            throw new Zend_Pdf_Exception( 'PDF named destination entry must be an array or dictionary.' );
-        }
-    }
-
-
-    /**
      * Creates or loads PDF document.
      *
      * If $source is null, then it creates a new document.
@@ -339,6 +349,9 @@ class Zend_Pdf
                 $this->_loadPages($this->_trailer->Root->Pages);
             }
 
+            $this->_loadNamedDestinations($this->_trailer->Root, $this->_parser->getPDFVersion());
+            $this->_loadOutlines($this->_trailer->Root);
+
             if ($this->_trailer->Info !== null) {
                 $this->properties = $this->_trailer->Info->toPhp();
 
@@ -365,77 +378,6 @@ class Zend_Pdf
 
                 $this->_originalProperties = $this->properties;
             }
-
-            // Collect named destinations (exclude not referenced pages)
-            $root = $this->_trailer->Root;
-
-            $pdfHeaderVersion = $this->_parser->getPDFVersion();
-            if ($root->Version !== null  &&  version_compare($root->Version->value, $pdfHeaderVersion, '>')) {
-                $versionIs_1_2_plus = version_compare($root->Version->value,    '1.1', '>');
-            } else {
-                $versionIs_1_2_plus = version_compare($pdfHeaderVersion, '1.1', '>');
-            }
-
-            if ($versionIs_1_2_plus) {
-                // PDF version is 1.2+
-                // Look for Destinations structure at Name dictionary
-                if ($root->Names !== null  &&  $root->Names->Dests !== null) {
-                    $intermediateNodes = array();
-                    $leafNodes         = array();
-                    if ($root->Names->Dests->Kids !== null) {
-                        $intermediateNodes[] = $root->Names->Dests;
-                    } else {
-                        $leafNodes[] = $root->Names->Dests;
-                    }
-
-                    while (count($intermediateNodes) != 0) {
-                        $newIntermediateNodes = array();
-                        foreach ($intermediateNodes as $node) {
-                            foreach ($node->Kids->items as $childNode) {
-                                if ($childNode->Kids !== null) {
-                                    $newIntermediateNodes[] = $childNode;
-                                } else {
-                                    $leafNodes[] = $childNode;
-                                }
-                            }
-                        }
-                        $intermediateNodes = $newIntermediateNodes;
-                    }
-
-                    foreach ($leafNodes as $leafNode) {
-                        $destinationsCount = count($leafNode->items)/2;
-                        for ($count = 0; $count < $destinationsCount; $count++) {
-                            $destinationName = $leafNode->items[$count*2];
-                            $destination     = $this->_loadDestination($leafNode->items[$count*2 + 1]);
-
-                            if ($destination instanceof Zend_Pdf_Action) {
-                                $this->_namedActions[$destKey]      = $destination;
-                            } else {
-                                $this->_namedDestinations[$destKey] = $destination;
-                            }
-                        }
-                    }
-                }
-            } else {
-                // PDF version is 1.1 (or earlier)
-                // Look for Destinations sructure at Dest entry of document catalog
-                if ($root->Dests !== null) {
-                    if ($root->Dests->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
-                        require_once 'Zend/Pdf/Exception.php';
-                        throw new Zend_Pdf_Exception( 'Document catalog Dests entry must be a dictionary.' );
-                    }
-
-                    foreach ($root->Dests->getKeys() as $destKey) {
-                        $destination = $this->_loadDestination($root->Dests->$destKey);
-
-                        if ($destination instanceof Zend_Pdf_Action) {
-                        	$this->_namedActions[$destKey]      = $destination;
-                        } else {
-                        	$this->_namedDestinations[$destKey] = $destination;
-                        }
-                    }
-                }
-            }
         } else {
             $this->_pdfHeaderVersion = Zend_Pdf::PDF_VERSION;
 
@@ -481,9 +423,9 @@ class Zend_Pdf
      */
     public function __destruct()
     {
-    	foreach ($this->_namedActions as $action) {
-    		$action->clean();
-    	}
+        foreach ($this->_namedActions as $action) {
+            $action->clean();
+        }
     }
 
     /**
@@ -562,7 +504,8 @@ class Zend_Pdf
                          * If any attribute or dependant object is an indirect object, then it's still
                          * shared between pages.
                          */
-                        if ($attributes[$property] instanceof Zend_Pdf_Element_Object) {
+                        if ($attributes[$property] instanceof Zend_Pdf_Element_Object  ||
+                            $attributes[$property] instanceof Zend_Pdf_Element_Reference) {
                             $child->$property = $attributes[$property];
                         } else {
                             $child->$property = $this->_objFactory->newObject($attributes[$property]);
@@ -575,6 +518,125 @@ class Zend_Pdf
     }
 
     /**
+     * Load named destinations recursively
+     *
+     * @param Zend_Pdf_Element_Reference $root Document catalog entry
+     * @param string $pdfHeaderVersion
+     * @throws Zend_Pdf_Exception
+     */
+    protected function _loadNamedDestinations(Zend_Pdf_Element_Reference $root, $pdfHeaderVersion)
+    {
+        if ($root->Version !== null  &&  version_compare($root->Version->value, $pdfHeaderVersion, '>')) {
+            $versionIs_1_2_plus = version_compare($root->Version->value,    '1.1', '>');
+        } else {
+            $versionIs_1_2_plus = version_compare($pdfHeaderVersion, '1.1', '>');
+        }
+
+        if ($versionIs_1_2_plus) {
+            // PDF version is 1.2+
+            // Look for Destinations structure at Name dictionary
+            if ($root->Names !== null  &&  $root->Names->Dests !== null) {
+                $intermediateNodes = array();
+                $leafNodes         = array();
+                if ($root->Names->Dests->Kids !== null) {
+                    $intermediateNodes[] = $root->Names->Dests;
+                } else {
+                    $leafNodes[] = $root->Names->Dests;
+                }
+
+                while (count($intermediateNodes) != 0) {
+                    $newIntermediateNodes = array();
+                    foreach ($intermediateNodes as $node) {
+                        foreach ($node->Kids->items as $childNode) {
+                            if ($childNode->Kids !== null) {
+                                $newIntermediateNodes[] = $childNode;
+                            } else {
+                                $leafNodes[] = $childNode;
+                            }
+                        }
+                    }
+                    $intermediateNodes = $newIntermediateNodes;
+                }
+
+                foreach ($leafNodes as $leafNode) {
+                    $destinationsCount = count($leafNode->items)/2;
+                    for ($count = 0; $count < $destinationsCount; $count++) {
+                        $destinationName = $leafNode->items[$count*2];
+                        $destination     = Zend_Pdf_Target::load($leafNode->items[$count*2 + 1]);
+
+                        if ($destination instanceof Zend_Pdf_Action) {
+                            $this->_namedActions[$destKey]      = $destination;
+                        } else {
+                            $this->_namedDestinations[$destKey] = $destination;
+                        }
+                    }
+                }
+            }
+        } else {
+            // PDF version is 1.1 (or earlier)
+            // Look for Destinations sructure at Dest entry of document catalog
+            if ($root->Dests !== null) {
+                if ($root->Dests->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
+                    require_once 'Zend/Pdf/Exception.php';
+                    throw new Zend_Pdf_Exception('Document catalog Dests entry must be a dictionary.');
+                }
+
+                foreach ($root->Dests->getKeys() as $destKey) {
+                    $destination = Zend_Pdf_Target::load($root->Dests->$destKey);
+
+                    if ($destination instanceof Zend_Pdf_Action) {
+                        $this->_namedActions[$destKey]      = $destination;
+                    } else {
+                        $this->_namedDestinations[$destKey] = $destination;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Load outlines recursively
+     *
+     * @param Zend_Pdf_Element_Reference $root Document catalog entry
+     */
+    protected function _loadOutlines(Zend_Pdf_Element_Reference $root)
+    {
+        if ($root->Outlines === null) {
+            return;
+        }
+
+        if ($root->Outlines->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception('Document catalog Outlines entry must be a dictionary.');
+        }
+
+        if ($root->Outlines->Type !== null  &&  $root->Outlines->Type->value != 'Outlines') {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception('Outlines Type entry must be an \'Outlines\' string.');
+        }
+
+        if ($root->Outlines->First === null) {
+            return;
+        }
+
+        $outlineDictionary = $root->Outlines->First;
+        $processedDictionaries = new SplObjectStorage();
+        while ($outlineDictionary !== null  &&  !$processedDictionaries->contains($outlineDictionary)) {
+            $processedDictionaries->attach($outlineDictionary);
+
+            $this->outlines[] = new Zend_Pdf_Outline_Loaded($outlineDictionary);
+
+            $outlineDictionary = $outlineDictionary->Next;
+        }
+
+        $this->_originalOutlines = $this->outlines;
+
+        if ($root->Outlines->Count !== null) {
+        	$this->_originalOpenOutlinesCount = $root->Outlines->Count->value;
+        }
+    }
+
+    /**
      * Orginize pages to tha pages tree structure.
      *
      * @todo atomatically attach page to the document, if it's not done yet.
@@ -590,7 +652,7 @@ class Zend_Pdf
         $pagesContainer->touch();
         $pagesContainer->Kids->items->clear();
 
-        $pageDictionaries = new SplObjectStorage();
+        $pageDictionaries = array();
         foreach ($this->pages as $page ) {
             $page->render($this->_objFactory);
 
@@ -600,8 +662,8 @@ class Zend_Pdf
 
             $pagesContainer->Kids->items[] = $pageDictionary;
 
-            // Collect page dictionary
-            $pageDictionaries->attach($pageDictionary);
+            // Collect page dictionary references
+            $pageDictionaries[$pageDictionary->toString()] = 1;
         }
 
         $pagesContainer->Count->touch();
@@ -609,10 +671,10 @@ class Zend_Pdf
 
         // Refresh named actions list
         foreach ($this->_namedActions as $name => $namedAction) {
-        	$rootAction = $namedAction;
+            $rootAction = $namedAction;
 
-        	// Walk through chained actions
-        	foreach ($namedAction->getAllActions() as $chainedAction) {
+            // Walk through chained actions
+            foreach ($namedAction->getAllActions() as $chainedAction) {
                 if ($chainedAction instanceof Zend_Pdf_Action_GoTo) {
                     $destination = $chainedAction->getDestination();
                     if (!$destination instanceof Zend_Pdf_Destination) {
@@ -620,42 +682,77 @@ class Zend_Pdf
                         throw new Zend_Pdf_Exception('PDF named actions (destinations) must refer target as an explicit destination.');
                     }
 
-                    $target = $destination->getTarget();
-                    if ($target instanceof Zend_Pdf_Element) {
-                        if (!$pageDictionaries->contains($target)) {
+                    $target = $destination->getResource()->items[0];
+                    if ($target->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
+                        if ($target->value > count($this->pages)) {
+                            $rootAction = $chainedAction->extract();
+                        }
+                    } else {
+                        if (!isset($pageDictionaries[$target->toString()])) {
                             $rootAction = $chainedAction->extract();
                         }
-                    } else if ($target > count($this->pages) ) {
-                        $rootAction = $chainedAction->extract();
                     }
                 }
-        	}
+            }
 
-        	if ($rootAction === null) {
-        		unset($this->_namedActions[$name]);
-        	} else {
-        		$rootAction->rebuildSubtree();
-        		$this->_namedActions[$name] = $rootAction;
-        	}
+            if ($rootAction === null) {
+                unset($this->_namedActions[$name]);
+            } else {
+                $rootAction->rebuildSubtree();
+                $this->_namedActions[$name] = $rootAction;
+            }
         }
 
         // Refresh named destinations list
         foreach ($this->_namedDestinations as $name => $destination) {
-            $target = $destination->getTarget();
-
-            if ($target instanceof Zend_Pdf_Element) {
-            	if (!$pageDictionaries->contains($target)) {
+            $target = $destination->getResource()->items[0];
+            if ($target->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
+                if ($target->value > count($this->pages) ) {
+                    unset($this->_namedDestinations[$name]);
+                }
+            } else {
+                if (!isset($pageDictionaries[$target->toString()])) {
                     unset($this->_namedDestinations[$name]);
-            	}
-            } else if ($target > count($this->pages) ) {
-                unset($this->_namedDestinations[$name]);
+                }
             }
         }
 
+        // Refresh outlines
+        $iterator = new RecursiveIteratorIterator(new Zend_Pdf_Outline_Container($this->outlines), RecursiveIteratorIterator::SELF_FIRST);
+        foreach ($iterator as $outline) {
+            $target = $outline->getTarget();
+
+            if ($target !== null) {
+                if (is_string($target)) {
+                    if (!isset($this->_namedActions[$target])  &&  !isset($this->_namedDestinations[$target])) {
+                        $outline->setTarget(null);
+                    }
+                } else if ($target instanceof Zend_Pdf_Destination) {
+                    $destinationPage = $target->getResource()->items[0];
+                    if ($destinationPage->getType() == Zend_Pdf_Element::TYPE_DICTIONARY) {
+                        if (!isset($pageDictionaries[$destinationPage->toString()])) {
+                            $outline->setTarget(null);
+                        }
+                    } else if ($destinationPage->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
+                        if ($destinationPage->value > count($this->pages)) {
+                            $outline->setTarget(null);
+                        }
+                    } else {
+                        require_once 'Zend/Pdf/Exception.php';
+                        throw new Zend_Pdf_Exception('Wrong destination page reference.');
+                    }
+                } else if ($target instanceof Zend_Pdf_Action) {
+                    /** @todo Implementation using iteration */
+                } else {
+                    require_once 'Zend/Pdf/Exception.php';
+                    throw new Zend_Pdf_Exception('Wrong outline target.');
+                }
+            }
+        }
 
         $openAction = $this->getOpenAction();
         if ($openAction !== null) {
-        	if ($openAction instanceof Zend_Pdf_Action) {
+            if ($openAction instanceof Zend_Pdf_Action) {
                 $rootAction = $openAction;
 
                 // Walk through chained actions
@@ -663,48 +760,51 @@ class Zend_Pdf
                     if ($chainedAction instanceof Zend_Pdf_Action_GoTo) {
                         $destination = $chainedAction->getDestination();
                         if (!$destination instanceof Zend_Pdf_Destination) {
-                        	// Look for $destination within named destinations
+                            // Look for $destination within named destinations
                             if (!isset($this->_namedActions[$destination])  &&  !isset($this->_namedDestinations[$destination])) {
-                            	$rootAction = $chainedAction->extract();
+                                $rootAction = $chainedAction->extract();
                             }
                         } else {
-                        	// Destination is Zend_Pdf_Destination object
-                            $target = $destination->getTarget();
-                            if ($target instanceof Zend_Pdf_Element) {
-                            	// which refers some page dictionary object
-                            	// (check if it's within a collected dictionaries for current document)
-                                if (!$pageDictionaries->contains($target)) {
+                            // Destination is Zend_Pdf_Destination object
+                            $target = $destination->getResource()->items[0];
+                            if ($target->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
+                                if ($target->value > count($this->pages) ) {
+                                    // it's a page number, check if we have enough pages
+                                    $rootAction = $chainedAction->extract();
+                                }
+                            } else {
+                                // which refers some page dictionary object
+                                // (check if it's within a collected dictionaries for current document)
+                                if (!isset($pageDictionaries[$target->toString()])) {
                                     $rootAction = $chainedAction->extract();
                                 }
-                            } else if ($target > count($this->pages) ) {
-                            	// it's a page number, check if we have enough pages
-                                $rootAction = $chainedAction->extract();
                             }
                         }
                     }
                 }
 
                 if ($rootAction !== null) {
-                	$rootAction->rebuildSubtree();
-                	$this->setOpenAction($rootAction);
+                    $rootAction->rebuildSubtree();
+                    $this->setOpenAction($rootAction);
                     $rootAction->clean();
                 } else {
                     $this->setOpenAction(null);
                 }
-        	} else if ($openAction instanceof Zend_Pdf_Destination) {
-                $target = $openAction->getTarget();
-
-                if ($target instanceof Zend_Pdf_Element) {
-                    if (!$pageDictionaries->contains($target)) {
+            } else if ($openAction instanceof Zend_Pdf_Destination) {
+                $target = $openAction->getResource()->items[0];
+                if ($target->getType() == Zend_Pdf_Element::TYPE_NUMERIC) {
+                    if ($target->value > count($this->pages) ) {
+                        $this->setOpenAction(null);
+                    }
+                } else {
+                    if (!isset($pageDictionaries[$target->toString()])) {
                         $this->setOpenAction(null);
                     }
-                } else if ($target > count($this->pages) ) {
-                    $this->setOpenAction(null);
                 }
-        	} else {
+            } else {
                 require_once 'Zend/Pdf/Exception.php';
                 throw new Zend_Pdf_Exception('OpenAction has to be either PDF Action or Destination.');
-        	}
+            }
         }
     }
 
@@ -721,16 +821,14 @@ class Zend_Pdf
         $destArray = $this->_objFactory->newObject(new Zend_Pdf_Element_Array());
         $destArrayItems = $destArray->items;
         foreach ($namedDestinations as $name => $destination) {
-        	$destArrayItems[] = new Zend_Pdf_Element_String($name);
+            $destArrayItems[] = new Zend_Pdf_Element_String($name);
 
-        	if ($destination instanceof Zend_Pdf_Action) {
-        		$destArrayItems[] = $destination->getDictionary();
-        	} else if ($destination instanceof Zend_Pdf_Destination) {
-        		$destArrayItems[] = $destination->getDestinationArray();
-        	} else {
+            if ($destination instanceof Zend_Pdf_Target) {
+                $destArrayItems[] = $destination->getResource();
+            } else {
                 require_once 'Zend/Pdf/Exception.php';
-                throw new Zend_Pdf_Exception('PDF named destinations must be Zend_Pdf_Action or Zend_Pdf_Destination objects.');
-        	}
+                throw new Zend_Pdf_Exception('PDF named destinations must be a Zend_Pdf_Target object.');
+            }
         }
 
         $DestTree = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
@@ -748,6 +846,72 @@ class Zend_Pdf
     }
 
     /**
+     * Dump outlines recursively
+     */
+    protected function _dumpOutlines()
+    {
+        $root = $this->_trailer->Root;
+
+        if ($root->Outlines === null) {
+        	if (count($this->outlines) == 0) {
+                return;
+        	} else {
+        		$root->Outlines = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
+        		$root->Outlines->Type = new Zend_Pdf_Element_Name('Outlines');
+        		$updateOutlinesNavigation = true;
+        	}
+        } else {
+            $updateOutlinesNavigation = false;
+            if (count($this->_originalOutlines) != count($this->outlines)) {
+                // If original and current outlines arrays have different size then outlines list was updated
+                $updateOutlinesNavigation = true;
+            } else if ( !(array_keys($this->_originalOutlines) === array_keys($this->outlines)) ) {
+                // If original and current outlines arrays have different keys (with a glance to an order) then outlines list was updated
+                $updateOutlinesNavigation = true;
+            } else {
+                foreach ($this->outlines as $key => $outline) {
+                    if ($this->_originalOutlines[$key] !== $outline) {
+                        $updateOutlinesNavigation = true;
+                    }
+                }
+            }
+        }
+
+        $lastOutline = null;
+        $openOutlinesCount = 0;
+        if ($updateOutlinesNavigation) {
+            $root->Outlines->touch();
+            $root->Outlines->First = null;
+
+            foreach ($this->outlines as $outline) {
+                if ($lastOutline === null) {
+                    // First pass. Update Outlines dictionary First entry using corresponding value
+                    $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines);
+                    $root->Outlines->First = $lastOutline;
+                } else {
+                    // Update previous outline dictionary Next entry (Prev is updated within dumpOutline() method)
+                    $currentOutlineDictionary = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline);
+                    $lastOutline->Next = $currentOutlineDictionary;
+                    $lastOutline       = $currentOutlineDictionary;
+                }
+                $openOutlinesCount += $outline->openOutlinesCount();
+            }
+
+            $root->Outlines->Last  = $lastOutline;
+        } else {
+            foreach ($this->outlines as $outline) {
+                $lastOutline = $outline->dumpOutline($this->_objFactory, $updateOutlinesNavigation, $root->Outlines, $lastOutline);
+                $openOutlinesCount += $outline->openOutlinesCount();
+            }
+        }
+
+        if ($openOutlinesCount != $this->_originalOpenOutlinesCount) {
+            $root->Outlines->touch;
+            $root->Outlines->Count = new Zend_Pdf_Element_Numeric($openOutlinesCount);
+        }
+    }
+
+    /**
      * Create page object, attached to the PDF document.
      * Method signatures:
      *
@@ -828,9 +992,9 @@ class Zend_Pdf
     public function getOpenAction()
     {
         if ($this->_trailer->Root->OpenAction !== null) {
-        	return $this->_loadDestination($this->_trailer->Root->OpenAction);
+            return Zend_Pdf_Target::load($this->_trailer->Root->OpenAction);
         } else {
-        	return null;
+            return null;
         }
     }
 
@@ -845,15 +1009,13 @@ class Zend_Pdf
         $root = $this->_trailer->Root;
         $root->touch();
 
-        if ($openAction instanceof Zend_Pdf_Destination) {
-        	$root->OpenAction = $openAction->getDestinationArray();
-        } if ($openAction instanceof Zend_Pdf_Action) {
-        	$root->OpenAction = $openAction->getDictionary();
-        } if ($openAction === null) {
-        	$root->OpenAction = null;
+        if ($openAction === null) {
+            $root->OpenAction = null;
+        } else if ($openAction instanceof Zend_Pdf_Target) {
+            $root->OpenAction = $openAction->getResource();
         } else {
             require_once 'Zend/Pdf/Exception.php';
-            throw new Zend_Pdf_Exception('Open action must be a Zend_Pdf_Destination or Zend_Pdf_Action objects or null.');
+            throw new Zend_Pdf_Exception('Open action must be a Zend_Pdf_Target or null.');
         }
     }
 
@@ -877,11 +1039,11 @@ class Zend_Pdf
      */
     public function getNamedAction($name)
     {
-    	if (isset($this->_namedActions[$name])) {
-    		return $this->_namedActions[$name];
-    	} else {
-    		return null;
-    	}
+        if (isset($this->_namedActions[$name])) {
+            return $this->_namedActions[$name];
+        } else {
+            return null;
+        }
     }
 
     /**
@@ -892,22 +1054,22 @@ class Zend_Pdf
      */
     public function setNamedAction($name, Zend_Pdf_Action_GoTo $action)
     {
-    	if (isset($this->_namedActions[$name])) {
-    		$this->_namedActions[$name]->clean();
-    	}
-    	// Clean corresponding named destination if set
+        if (isset($this->_namedActions[$name])) {
+            $this->_namedActions[$name]->clean();
+        }
+        // Clean corresponding named destination if set
         unset($this->_namedDestinations[$name]);
 
-    	if (!$action->getDestination() instanceof Zend_Pdf_Destination) {
+        if (!$action->getDestination() instanceof Zend_Pdf_Destination) {
             require_once 'Zend/Pdf/Exception.php';
             throw new Zend_Pdf_Exception('PDF named actions (destinations) must refer target as an explicit destination.');
-    	}
+        }
 
-    	if ($action !== null) {
+        if ($action !== null) {
             $this->_namedActions[$name] = $action;
-    	} else {
-    		unset($this->_namedActions[$name]);
-    	}
+        } else {
+            unset($this->_namedActions[$name]);
+        }
     }
 
     /**
@@ -928,11 +1090,11 @@ class Zend_Pdf
      */
     public function getNamedDestination($name)
     {
-    	if (isset($this->_namedDestinations[$name])) {
-    		return $this->_namedDestinations[$name];
-    	} else {
-    		return null;
-    	}
+        if (isset($this->_namedDestinations[$name])) {
+            return $this->_namedDestinations[$name];
+        } else {
+            return null;
+        }
     }
 
     /**
@@ -944,15 +1106,15 @@ class Zend_Pdf
     public function setNamedDestination($name, Zend_Pdf_Destination $destination)
     {
         // Clean corresponding named action if set
-    	if (isset($this->_namedActions[$name])) {
+        if (isset($this->_namedActions[$name])) {
             $this->_namedActions[$name]->clean();
             unset($this->_namedActions[$name]);
         }
 
         if ($destination !== null) {
-    	   $this->_namedDestinations[$name] = $destination;
+           $this->_namedDestinations[$name] = $destination;
         } else {
-        	unset($this->_namedDestinations[$name]);
+            unset($this->_namedDestinations[$name]);
         }
     }
 
@@ -1139,6 +1301,7 @@ class Zend_Pdf
 
         $this->_dumpPages();
         $this->_dumpNamedDestinations();
+        $this->_dumpOutlines();
 
         // Check, that PDF file was modified
         // File is always modified by _dumpPages() now, but future implementations may eliminate this.

+ 8 - 8
library/Zend/Pdf/Action.php

@@ -23,6 +23,9 @@
 /** Zend_Pdf_ElementFactory */
 require_once 'Zend/Pdf/ElementFactory.php';
 
+/** Zend_Pdf_Target */
+require_once 'Zend/Pdf/Target.php';
+
 
 /**
  * Abstract PDF action representation class
@@ -32,7 +35,7 @@ require_once 'Zend/Pdf/ElementFactory.php';
  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  * @license    http://framework.zend.com/license/new-bsd     New BSD License
  */
-abstract class Zend_Pdf_Action
+abstract class Zend_Pdf_Action extends Zend_Pdf_Target
 {
 	/**
 	 * Action dictionary
@@ -109,13 +112,10 @@ abstract class Zend_Pdf_Action
 	 * @return Zend_Pdf_Action
 	 * @throws Zend_Pdf_Exception
 	 */
-	public static function load(Zend_Pdf_Element $dictionary, $parentAction = null, $processedActions = null)
+	public static function load(Zend_Pdf_Element $dictionary, $parentAction = null, SplObjectStorage $processedActions = null)
 	{
         if ($processedActions === null) {
             $processedActions = new SplObjectStorage();
-        } else if (!$processedActions instanceof SplObjectStorage) {
-            require_once 'Zend/Pdf/Exception.php';
-            throw new Zend_Pdf_Exception('$processedActions parameter must be a SplObjectStorage object or null.');
         }
 
         if ($dictionary->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
@@ -390,12 +390,12 @@ abstract class Zend_Pdf_Action
     }
 
     /**
-     * Get Action dictionary
+     * Get resource
      *
      * @internal
-     * @return Zend_Pdf_Element_Dictionary|Zend_Pdf_Element_Object|Zend_Pdf_Element_Reference
+     * @return Zend_Pdf_Element
      */
-    public function getDictionary()
+    public function getResource()
     {
     	return $this->_actionDictionary;
     }

+ 1 - 1
library/Zend/Pdf/Action/GoTo.php

@@ -71,7 +71,7 @@ class Zend_Pdf_Action_GoTo extends Zend_Pdf_Action
     		// DestinationArray
             $dictionary->D = $destination;
     	} else if ($destination instanceof Zend_Pdf_Destination) {
-    		$dictionary->D = $destination->getDestinationArray();
+    		$dictionary->D = $destination->getResource();
     	} else {
     		require_once 'Zend/Pdf/Exception.php';
     		throw new Zend_Pdf_Exception('Wrong $destination parameter type');

+ 7 - 0
library/Zend/Pdf/Color.php

@@ -42,5 +42,12 @@ abstract class Zend_Pdf_Color
      * @return string
      */
     abstract public function instructions($stroking);
+
+    /**
+     * Get color components (color space dependent)
+     *
+     * @return array
+     */
+    abstract public function getComponents();
 }
 

+ 22 - 12
library/Zend/Pdf/Color/Cmyk.php

@@ -78,22 +78,22 @@ class Zend_Pdf_Color_Cmyk extends Zend_Pdf_Color
      */
     public function __construct($c, $m, $y, $k)
     {
-        $this->_c = new Zend_Pdf_Element_Numeric($c);
-        $this->_m = new Zend_Pdf_Element_Numeric($m);
-        $this->_y = new Zend_Pdf_Element_Numeric($y);
-        $this->_k = new Zend_Pdf_Element_Numeric($k);
+        if ($c < 0) { $c = 0; }
+        if ($c > 1) { $c = 1; }
 
-        if ($this->_c->value < 0) { $this->_c->value = 0; }
-        if ($this->_c->value > 1) { $this->_c->value = 1; }
+        if ($m < 0) { $m = 0; }
+        if ($m > 1) { $m = 1; }
 
-        if ($this->_m->value < 0) { $this->_m->value = 0; }
-        if ($this->_m->value > 1) { $this->_m->value = 1; }
+        if ($y < 0) { $y = 0; }
+        if ($y > 1) { $y = 1; }
 
-        if ($this->_y->value < 0) { $this->_y->value = 0; }
-        if ($this->_y->value > 1) { $this->_y->value = 1; }
+        if ($k < 0) { $k = 0; }
+        if ($k > 1) { $k = 1; }
 
-        if ($this->_k->value < 0) { $this->_k->value = 0; }
-        if ($this->_k->value > 1) { $this->_k->value = 1; }
+        $this->_c = new Zend_Pdf_Element_Numeric($c);
+        $this->_m = new Zend_Pdf_Element_Numeric($m);
+        $this->_y = new Zend_Pdf_Element_Numeric($y);
+        $this->_k = new Zend_Pdf_Element_Numeric($k);
     }
 
     /**
@@ -111,5 +111,15 @@ class Zend_Pdf_Color_Cmyk extends Zend_Pdf_Color
              . $this->_y->toString() . ' '
              . $this->_k->toString() .     ($stroking? " K\n" : " k\n");
     }
+
+    /**
+     * Get color components (color space dependent)
+     *
+     * @return array
+     */
+    public function getComponents()
+    {
+        return array($this->_c->value, $this->_m->value, $this->_y->value, $this->_k->value);
+    }
 }
 

+ 13 - 8
library/Zend/Pdf/Color/GrayScale.php

@@ -50,15 +50,10 @@ class Zend_Pdf_Color_GrayScale extends Zend_Pdf_Color
      */
     public function __construct($grayLevel)
     {
-        $this->_grayLevel = new Zend_Pdf_Element_Numeric($grayLevel);
-
-        if ($this->_grayLevel->value < 0) {
-            $this->_grayLevel->value = 0;
-        }
+        if ($grayLevel < 0) { $grayLevel = 0; }
+        if ($grayLevel > 1) { $grayLevel = 1; }
 
-        if ($this->_grayLevel->value > 1) {
-            $this->_grayLevel->value = 1;
-        }
+        $this->_grayLevel = new Zend_Pdf_Element_Numeric($grayLevel);
     }
 
     /**
@@ -73,5 +68,15 @@ class Zend_Pdf_Color_GrayScale extends Zend_Pdf_Color
     {
         return $this->_grayLevel->toString() . ($stroking? " G\n" : " g\n");
     }
+
+    /**
+     * Get color components (color space dependent)
+     *
+     * @return array
+     */
+    public function getComponents()
+    {
+        return array($this->_grayLevel->value);
+    }
 }
 

+ 10 - 0
library/Zend/Pdf/Color/Html.php

@@ -75,6 +75,16 @@ class Zend_Pdf_Color_Html extends Zend_Pdf_Color
     }
 
     /**
+     * Get color components (color space dependent)
+     *
+     * @return array
+     */
+    public function getComponents()
+    {
+        return $this->_color->getComponents();
+    }
+
+    /**
      * Creates a Zend_Pdf_Color object from the HTML representation.
      *
      * @param string $color May either be a hexidecimal number of the form

+ 10 - 0
library/Zend/Pdf/Color/Rgb.php

@@ -98,5 +98,15 @@ class Zend_Pdf_Color_Rgb extends Zend_Pdf_Color
              . $this->_g->toString() . ' '
              . $this->_b->toString() .     ($stroking? " RG\n" : " rg\n");
     }
+
+    /**
+     * Get color components (color space dependent)
+     *
+     * @return array
+     */
+    public function getComponents()
+    {
+        return array($this->_r->value, $this->_g->value, $this->_b->value);
+    }
 }
 

+ 10 - 10
library/Zend/Pdf/Destination.php

@@ -26,6 +26,9 @@ require_once 'Zend/Pdf/ElementFactory.php';
 /** Zend_Pdf_Page */
 require_once 'Zend/Pdf/Page.php';
 
+/** Zend_Pdf_Target */
+require_once 'Zend/Pdf/Target.php';
+
 
 /**
  * Abstract PDF explicit destination representation class
@@ -35,7 +38,7 @@ require_once 'Zend/Pdf/Page.php';
  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  * @license    http://framework.zend.com/license/new-bsd     New BSD License
  */
-abstract class Zend_Pdf_Destination
+abstract class Zend_Pdf_Destination extends Zend_Pdf_Target
 {
 	/**
 	 * Destination description array
@@ -171,25 +174,22 @@ abstract class Zend_Pdf_Destination
      * Returns page number for remote destinations and
      * page dictionary object reference otherwise
      *
+     * @internal
      * @return integer|Zend_Pdf_Element_Dictionary
      */
     public function getTarget()
     {
-    	if ($this->isRemote()) {
-            return $this->_destinationArray->items[0]->value;
-    	} else {
-    		return $this->_destinationArray->items[0];
-    	}
+        return $this->_destinationArray->items[0];
     }
 
     /**
-     * Get destination array object
+     * Get resource
      *
      * @internal
-     * @return Zend_Pdf_Element_Array|Zend_Pdf_Element_Object|Zend_Pdf_Element_Reference
+     * @return Zend_Pdf_Element
      */
-    public function getDestinationArray()
+    public function getResource()
     {
-    	return $this->_destinationArray;
+        return $this->_destinationArray;
     }
 }

+ 2 - 2
library/Zend/Pdf/Element.php

@@ -74,9 +74,9 @@ abstract class Zend_Pdf_Element
      *
      * @param Zend_Pdf_Element_Object $parent
      */
-    public function setParentObject(Zend_Pdf_Element_Object &$parent)
+    public function setParentObject(Zend_Pdf_Element_Object $parent)
     {
-        $this->_parentObject = &$parent;
+        $this->_parentObject = $parent;
     }
 
 

+ 1 - 1
library/Zend/Pdf/Element/Numeric.php

@@ -55,7 +55,7 @@ class Zend_Pdf_Element_Numeric extends Zend_Pdf_Element
             throw new Zend_Pdf_Exception('Argument must be numeric');
         }
 
-        $this->value   = $val;
+        $this->value = $val;
     }
 
 

+ 5 - 16
library/Zend/Pdf/Element/Object.php

@@ -77,7 +77,7 @@ class Zend_Pdf_Element_Object extends Zend_Pdf_Element
     public function __construct(Zend_Pdf_Element $val, $objNum, $genNum, Zend_Pdf_ElementFactory $factory)
     {
         if ($val instanceof self) {
-            throw new Zend_Pdf_Exception('Object number must not be instance of Zend_Pdf_Element_Object.');
+            throw new Zend_Pdf_Exception('Object number must not be an instance of Zend_Pdf_Element_Object.');
         }
 
         if ( !(is_integer($objNum) && $objNum > 0) ) {
@@ -93,7 +93,9 @@ class Zend_Pdf_Element_Object extends Zend_Pdf_Element
         $this->_genNum  = $genNum;
         $this->_factory = $factory;
 
-        $factory->registerObject($this);
+        $this->setParentObject($this);
+
+        $factory->registerObject($this, $objNum . ' ' . $genNum);
     }
 
 
@@ -206,20 +208,7 @@ class Zend_Pdf_Element_Object extends Zend_Pdf_Element
      */
     public function __call($method, $args)
     {
-        switch (count($args)) {
-            case 0:
-                return $this->_value->$method();
-            case 1:
-                return $this->_value->$method($args[0]);
-            case 2:
-                return $this->_value->$method($args[0], $args[1]);
-            case 3:
-                return $this->_value->$method($args[0], $args[1], $args[2]);
-            case 4:
-                return $this->_value->$method($args[0], $args[1], $args[2], $args[3]);
-            default:
-                throw new Zend_Pdf_Exception('Unsupported number of arguments');
-        }
+    	return call_user_func_array(array($this->_value, $method), $args);
     }
 
 

+ 8 - 19
library/Zend/Pdf/Element/Reference.php

@@ -165,10 +165,12 @@ class Zend_Pdf_Element_Reference extends Zend_Pdf_Element
      */
     private function _dereference()
     {
-        $obj = $this->_context->getParser()->getObject(
-                       $this->_context->getRefTable()->getOffset($this->_objNum . ' ' . $this->_genNum . ' R'),
-                       $this->_context
-                                                      );
+    	if (($obj = $this->_factory->fetchObject($this->_objNum . ' ' . $this->_genNum)) === null) {
+            $obj = $this->_context->getParser()->getObject(
+                           $this->_context->getRefTable()->getOffset($this->_objNum . ' ' . $this->_genNum . ' R'),
+                           $this->_context
+                                                          );
+    	}
 
         if ($obj === null ) {
             $this->_ref = new Zend_Pdf_Element_Null();
@@ -182,7 +184,7 @@ class Zend_Pdf_Element_Reference extends Zend_Pdf_Element
         $this->_ref = $obj;
         $this->setParentObject($obj);
 
-        $this->_factory->registerObject($this);
+        $this->_factory->registerObject($obj, $this->_objNum . ' ' . $this->_genNum);
     }
 
     /**
@@ -241,20 +243,7 @@ class Zend_Pdf_Element_Reference extends Zend_Pdf_Element
             $this->_dereference();
         }
 
-        switch (count($args)) {
-            case 0:
-                return $this->_ref->$method();
-            case 1:
-                return $this->_ref->$method($args[0]);
-            case 2:
-                return $this->_ref->$method($args[0], $args[1]);
-            case 3:
-                return $this->_ref->$method($args[0], $args[1], $args[2]);
-            case 4:
-                return $this->_ref->$method($args[0], $args[1], $args[2], $args[3]);
-            default:
-                throw new Zend_Pdf_Exception('Unsupported number of arguments');
-        }
+        return call_user_func_array(array($this->_ref, $method), $args);
     }
 
     /**

+ 1 - 1
library/Zend/Pdf/Element/Reference/Context.php

@@ -69,7 +69,7 @@ class Zend_Pdf_Element_Reference_Context
     /**
      * Context parser
      *
-     * @return Zend_Pdf_Parser
+     * @return Zend_Pdf_StringParser
      */
     public function getParser()
     {

+ 18 - 2
library/Zend/Pdf/ElementFactory.php

@@ -413,14 +413,30 @@ class Zend_Pdf_ElementFactory implements Zend_Pdf_ElementFactory_Interface
      *
      * It's used to clear "parent object" referencies when factory is closed and clean up resources
      *
+     * @param string $refString
      * @param Zend_Pdf_Element_Object $obj
      */
-    public function registerObject($obj)
+    public function registerObject(Zend_Pdf_Element_Object $obj, $refString)
     {
-        $this->_registeredObjects[] = $obj;
+        $this->_registeredObjects[$refString] = $obj;
     }
 
     /**
+     * Fetch object specified by reference
+     *
+     * @param string $refString
+     * @return Zend_Pdf_Element_Object|null
+     */
+    public function fetchObject($refString)
+    {
+    	if (!isset($this->_registeredObjects[$refString])) {
+    		return null;
+    	}
+        return $this->_registeredObjects[$refString];
+    }
+
+
+    /**
      * Check if PDF file was modified
      *
      * @return boolean

+ 370 - 0
library/Zend/Pdf/Outline.php

@@ -0,0 +1,370 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** Zend_Pdf_ElementFactory */
+require_once 'Zend/Pdf/ElementFactory.php';
+
+
+/**
+ * Abstract PDF outline representation class
+ *
+ * @todo Implement an ability to associate an outline item with a structure element (PDF 1.3 feature)
+ *
+ * @package    Zend_Pdf
+ * @subpackage Outlines
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Pdf_Outline implements RecursiveIterator, Countable
+{
+	/**
+	 * True if outline is open.
+	 *
+	 * @var boolean
+	 */
+	protected $_open = false;
+
+    /**
+     * Array of child outlines (array of Zend_Pdf_Outline objects)
+     *
+     * @var array
+     */
+    public $childOutlines = array();
+
+
+    /**
+     * Get outline title.
+     *
+     * @return string
+     */
+    abstract public function getTitle();
+
+    /**
+     * Set outline title
+     *
+     * @param string $title
+     * @return Zend_Pdf_Outline
+     */
+    abstract public function setTitle($title);
+
+    /**
+	 * Returns true if outline item is open by default
+	 *
+	 * @return boolean
+	 */
+	public function isOpen()
+	{
+		return $this->_open;
+	}
+
+    /**
+     * Sets 'isOpen' outline flag
+     *
+     * @param boolean $isOpen
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsOpen($isOpen)
+    {
+        $this->_open = $isOpen;
+        return $this;
+    }
+
+    /**
+     * Returns true if outline item is displayed in italic
+     *
+     * @return boolean
+     */
+    abstract public function isItalic();
+
+    /**
+     * Sets 'isItalic' outline flag
+     *
+     * @param boolean $isItalic
+     * @return Zend_Pdf_Outline
+     */
+    abstract public function setIsItalic($isItalic);
+
+    /**
+     * Returns true if outline item is displayed in bold
+     *
+     * @return boolean
+     */
+    abstract public function isBold();
+
+    /**
+     * Sets 'isBold' outline flag
+     *
+     * @param boolean $isBold
+     * @return Zend_Pdf_Outline
+     */
+    abstract public function setIsBold($isBold);
+
+
+    /**
+     * Get outline text color.
+     *
+     * @return Zend_Pdf_Color_Rgb
+     */
+    abstract public function getColor();
+
+    /**
+     * Set outline text color.
+     * (null means default color which is black)
+     *
+     * @param Zend_Pdf_Color_Rgb $color
+     * @return Zend_Pdf_Outline
+     */
+    abstract public function setColor(Zend_Pdf_Color_Rgb $color);
+
+    /**
+     * Get outline target.
+     *
+     * @return Zend_Pdf_Destination|Zend_Pdf_Action|string
+     */
+    abstract public function getTarget();
+
+    /**
+     * Set outline target.
+     * Null means no target
+     *
+     * @param Zend_Pdf_Destination|Zend_Pdf_Action|string $target
+     * @return Zend_Pdf_Outline
+     */
+    abstract public function setTarget($target);
+
+    /**
+     * Get outline options
+     *
+     * @return array
+     */
+    public function getOptions()
+    {
+    	return array('title'  => $this->_title,
+    	             'open'   => $this->_open,
+    	             'color'  => $this->_color,
+    	             'italic' => $this->_italic,
+    	             'bold'   => $this->_bold,
+    	             'target' => $this->_target);
+    }
+
+	/**
+	 * Set outline options
+	 *
+	 * @param array $options
+	 * @return Zend_Pdf_Actions
+     * @throws Zend_Pdf_Exception
+	 */
+	public function setOptions(array $options)
+	{
+		foreach ($options as $key => $value) {
+			switch ($key) {
+                case 'title':
+                	$this->setTitle($value);
+                	break;
+
+                case 'open':
+                    $this->setIsOpen($value);
+                    break;
+
+                case 'color':
+                    $this->setColor($value);
+                    break;
+                case 'italic':
+                    $this->setIsItalic($value);
+                    break;
+
+                case 'bold':
+                    $this->setIsBold($value);
+                    break;
+
+                case 'target':
+                    $this->setTarget($value);
+                    break;
+
+                default:
+                    require_once 'Zend/Pdf/Exception.php';
+                    throw new Zend_Pdf_Exception("Unknown option name - '$key'.");
+                	break;
+			}
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Create new Outline object
+	 *
+	 * It provides two forms of input parameters:
+	 *
+	 * 1. Zend_Pdf_Outline::create(string $title[, Zend_Pdf_Destination $destination])
+	 * 2. Zend_Pdf_Outline::create(array $options)
+	 *
+	 * Second form allows to provide outline options as an array.
+	 * The followed options are supported:
+	 *   'title'  - string, outline title, required
+	 *   'open'   - boolean, true if outline entry is open (default value is false)
+	 *   'color'  - Zend_Pdf_Color_Rgb object, true if outline entry is open (default value is null - black)
+	 *   'italic' - boolean, true if outline entry is displayed in italic (default value is false)
+	 *   'bold'   - boolean, true if outline entry is displayed in bold (default value is false)
+	 *   'target' - Zend_Pdf_Destination object, outline item destination
+	 *
+	 * @return Zend_Pdf_Outline
+	 * @throws Zend_Pdf_Exception
+	 */
+	public static function create($param1, $param2 = null)
+	{
+		if (is_string($param1)) {
+			if ($param2 !== null  &&  !$param2 instanceof Zend_Pdf_Destination) {
+				require_once 'Zend/Pdf/Exception.php';
+				throw new Zend_Pdf_Exception('Outline create method takes $title (string) and $destination (Zend_Pdf_Destination) or an array as an input');
+			}
+
+			require_once 'Zend/Pdf/Outline/Created.php';
+			return new Zend_Pdf_Outline_Created(array('title'  => $param1,
+			                                          'target' => $param2));
+		} else {
+			if (!is_array($param1)  ||  $param2 !== null) {
+                require_once 'Zend/Pdf/Exception.php';
+                throw new Zend_Pdf_Exception('Outline create method takes $title (string) and $destination (Zend_Pdf_Destination) or an array as an input');
+			}
+
+			return new Zend_Pdf_Outline_Created($param1);
+		}
+	}
+
+	/**
+	 * Returns number of the total number of open items at all levels of the outline.
+	 *
+	 * @internal
+	 * @return integer
+	 */
+	public function openOutlinesCount()
+	{
+		$count = 1; // Include this outline
+
+		if ($this->isOpen()) {
+			foreach ($this->childOutlines as $child) {
+				$count += $child->openOutlinesCount();
+			}
+		}
+
+		return $count;
+	}
+
+	/**
+	 * Dump Outline and it's child outlines into PDF structures
+	 *
+	 * Returns dictionary indirect object or reference
+	 *
+	 * @param Zend_Pdf_ElementFactory $factory object factory for newly created indirect objects
+	 * @param boolean $updateNavigation
+	 * @param
+	 * @return Zend_Pdf_Element
+	 */
+	abstract public function dumpOutline(Zend_Pdf_ElementFactory_Interface $factory, $updateNavigation, Zend_Pdf_Element $parent, Zend_Pdf_Element $prev = null);
+
+
+	////////////////////////////////////////////////////////////////////////
+	//  RecursiveIterator interface methods
+	//////////////
+
+	/**
+     * Returns the child outline.
+     *
+     * @return Zend_Pdf_Outline|null
+     */
+    public function current()
+    {
+        return current($this->childOutlines);
+    }
+
+    /**
+     * Returns current iterator key
+     *
+     * @return integer
+     */
+    public function key()
+    {
+        return key($this->childOutlines);
+    }
+
+    /**
+     * Go to next child
+     */
+    public function next()
+    {
+        return next($this->childOutlines);
+    }
+
+    /**
+     * Rewind children
+     */
+    public function rewind()
+    {
+        return reset($this->childOutlines);
+    }
+
+    /**
+     * Check if current position is valid
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return current($this->childOutlines) !== false;
+    }
+
+	/**
+     * Returns the child outline.
+     *
+     * @return Zend_Pdf_Outline|null
+     */
+    public function getChildren()
+    {
+        return current($this->childOutlines);
+    }
+
+    /**
+     * Implements RecursiveIterator interface.
+     *
+     * @return bool  whether container has any pages
+     */
+    public function hasChildren()
+    {
+        return count($this->childOutlines) > 0;
+    }
+
+
+    ////////////////////////////////////////////////////////////////////////
+    //  Countable interface methods
+    //////////////
+
+    /**
+     * count()
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->childOutlines);
+    }
+}

+ 137 - 0
library/Zend/Pdf/Outline/Container.php

@@ -0,0 +1,137 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Iteratable outlines container
+ *
+ * @todo Implement an ability to associate an outline item with a structure element (PDF 1.3 feature)
+ *
+ * @package    Zend_Pdf
+ * @subpackage Outlines
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Pdf_Outline_Container implements RecursiveIterator, Countable
+{
+    /**
+     * Array of outlines (array of Zend_Pdf_Outline objects)
+     *
+     * @var array
+     */
+    protected $_outlines = array();
+
+
+    /**
+     * Object constructor
+     *
+     * @param array $outlines
+     */
+    public function __construct(array $outlines)
+    {
+    	$this->_outlines = $outlines;
+    }
+
+	////////////////////////////////////////////////////////////////////////
+	//  RecursiveIterator interface methods
+	//////////////
+
+	/**
+     * Returns the child outline.
+     *
+     * @return Zend_Pdf_Outline|null
+     */
+    public function current()
+    {
+        return current($this->_outlines);
+    }
+
+    /**
+     * Returns current iterator key
+     *
+     * @return integer
+     */
+    public function key()
+    {
+        return key($this->_outlines);
+    }
+
+    /**
+     * Go to next child
+     */
+    public function next()
+    {
+        return next($this->_outlines);
+    }
+
+    /**
+     * Rewind children
+     */
+    public function rewind()
+    {
+        return reset($this->_outlines);
+    }
+
+    /**
+     * Check if current position is valid
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return current($this->_outlines) !== false;
+    }
+
+	/**
+     * Returns the child outline.
+     *
+     * @return Zend_Pdf_Outline|null
+     */
+    public function getChildren()
+    {
+        return current($this->_outlines);
+    }
+
+    /**
+     * Implements RecursiveIterator interface.
+     *
+     * @return bool  whether container has any pages
+     */
+    public function hasChildren()
+    {
+        return count($this->_outlines) > 0;
+    }
+
+
+    ////////////////////////////////////////////////////////////////////////
+    //  Countable interface methods
+    //////////////
+
+    /**
+     * count()
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_outlines);
+    }
+}

+ 289 - 0
library/Zend/Pdf/Outline/Created.php

@@ -0,0 +1,289 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** Zend_Pdf_ElementFactory */
+require_once 'Zend/Pdf/ElementFactory.php';
+
+/** Zend_Pdf_Outline */
+require_once 'Zend/Pdf/Outline.php';
+
+
+/**
+ * PDF outline representation class
+ *
+ * @todo Implement an ability to associate an outline item with a structure element (PDF 1.3 feature)
+ *
+ * @package    Zend_Pdf
+ * @subpackage Outlines
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Pdf_Outline_Created extends Zend_Pdf_Outline
+{
+	/**
+	 * Outline title.
+	 *
+	 * @var string
+	 */
+	protected $_title;
+
+    /**
+     * Color to be used for the outline entry’s text.
+
+     * It uses the DeviceRGB color space for color representation.
+     * Null means default value - black ([0.0 0.0 0.0] in RGB representation).
+     *
+     * @var Zend_Pdf_Color_Rgb
+     */
+    protected $_color = null;
+
+    /**
+     * True if outline item is displayed in italic.
+     * Default value is false.
+     *
+     * @var boolean
+     */
+    protected $_italic = false;
+
+    /**
+     * True if outline item is displayed in bold.
+     * Default value is false.
+     *
+     * @var boolean
+     */
+    protected $_bold = false;
+
+    /**
+     * Target destination or action.
+     * String means named destination
+     *
+     * Null means no target.
+     *
+     * @var Zend_Pdf_Destination|Zend_Pdf_Action
+     */
+    protected $_target = null;
+
+
+    /**
+	 * Get outline title.
+	 *
+	 * @return string
+	 */
+	public function getTitle()
+	{
+		return $this->_title;
+	}
+
+	/**
+	 * Set outline title
+	 *
+	 * @param string $title
+     * @return Zend_Pdf_Outline
+	 */
+	public function setTitle($title)
+	{
+		$this->_title = $title;
+		return $this;
+	}
+
+    /**
+     * Returns true if outline item is displayed in italic
+     *
+     * @return boolean
+     */
+    public function isItalic()
+    {
+        return $this->_italic;
+    }
+
+    /**
+     * Sets 'isItalic' outline flag
+     *
+     * @param boolean $isItalic
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsItalic($isItalic)
+    {
+        $this->_italic = $isItalic;
+        return $this;
+    }
+
+    /**
+     * Returns true if outline item is displayed in bold
+     *
+     * @return boolean
+     */
+    public function isBold()
+    {
+        return $this->_bold;
+    }
+
+    /**
+     * Sets 'isBold' outline flag
+     *
+     * @param boolean $isBold
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsBold($isBold)
+    {
+        $this->_bold = $isBold;
+        return $this;
+    }
+
+
+    /**
+     * Get outline text color.
+     *
+     * @return Zend_Pdf_Color_Rgb
+     */
+    public function getColor()
+    {
+        return $this->_color;
+    }
+
+    /**
+     * Set outline text color.
+     * (null means default color which is black)
+     *
+     * @param Zend_Pdf_Color_Rgb $color
+     * @return Zend_Pdf_Outline
+     */
+    public function setColor(Zend_Pdf_Color_Rgb $color)
+    {
+        $this->_color = $color;
+        return $this;
+    }
+
+    /**
+     * Get outline target.
+     *
+     * @return Zend_Pdf_Destination|Zend_Pdf_Action|string
+     */
+    public function getTarget()
+    {
+        return $this->_target;
+    }
+
+    /**
+     * Set outline target.
+     * Null means no target
+     *
+     * @param Zend_Pdf_Destination|Zend_Pdf_Action|string $target
+     * @return Zend_Pdf_Outline
+     */
+    public function setTarget($target)
+    {
+    	if ($target !== null  && is_string($target) &&  !$target instanceof Zend_Pdf_Destination  &&  !$target instanceof Zend_Pdf_Action) {
+    		require_once 'Zend/Pdf/Exception.php';
+    		throw new Zend_Pdf_Exception('Outline target has to be Zend_Pdf_Destination or Zend_Pdf_Action object, string or null');
+    	}
+        $this->_target = $target;
+        return $this;
+    }
+
+
+	/**
+     * Object constructor
+     *
+     * @param array $options
+     * @throws Zend_Pdf_Exception
+	 */
+	public function __construct($options = array())
+	{
+		if (!isset($options['title'])) {
+			require_once 'Zend/Pdf/Exception.php';
+			throw new Zend_Pdf_Exception('Title parameter is required.');
+		}
+
+		$this->setOptions($options);
+	}
+
+    /**
+     * Dump Outline and it's child outlines into PDF structures
+     *
+     * Returns dictionary indirect object or reference
+     *
+     * @internal
+     * @param Zend_Pdf_ElementFactory    $factory object factory for newly created indirect objects
+     * @param boolean $updateNavigation  Update navigation flag
+     * @param Zend_Pdf_Element $parent   Parent outline dictionary reference
+     * @param Zend_Pdf_Element $prev     Previous outline dictionary reference
+     * @return Zend_Pdf_Element
+     */
+    public function dumpOutline(Zend_Pdf_ElementFactory_Interface $factory, $updateNavigation, Zend_Pdf_Element $parent, Zend_Pdf_Element $prev = null)
+    {
+    	$outlineDictionary = $factory->newObject(new Zend_Pdf_Element_Dictionary());
+
+    	$outlineDictionary->Title = new Zend_Pdf_Element_String($this->getTitle());
+
+    	$target = $this->getTarget();
+        if ($target === null) {
+            // Do nothing
+        } else if (is_string($target)) {
+            $outlineDictionary->Dest = new Zend_Pdf_Element_String($target);
+        } else if ($target instanceof Zend_Pdf_Destination) {
+            $outlineDictionary->Dest = $target->getTarget();
+        } else if ($target instanceof Zend_Pdf_Action) {
+            $outlineDictionary->A    = $target->getDestinationArray()->items[0];
+        } else {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception('Outline target has to be Zend_Pdf_Destination or Zend_Pdf_Action object, string or null');
+        }
+
+        $color = $this->getColor();
+        if ($color !== null) {
+            $components = $color->getComponents();
+            $colorComponentElements = array(new Zend_Pdf_Element_Numeric($components[0]),
+                                            new Zend_Pdf_Element_Numeric($components[1]),
+                                            new Zend_Pdf_Element_Numeric($components[2]));
+            $outlineDictionary->C = new Zend_Pdf_Element_Array($colorComponentElements);
+        }
+
+        if ($this->isItalic()  ||  $this->isBold()) {
+        	$outlineDictionary->F = new Zend_Pdf_Element_Numeric(($this->isItalic()? 1 : 0)  |   // Bit 1 - Italic
+        	                                                     ($this->isBold()?   2 : 0));    // Bit 2 - Bold
+        }
+
+
+        $outlineDictionary->Parent = $parent;
+        $outlineDictionary->Prev   = $prev;
+
+        $lastChild = null;
+        foreach ($this->childOutlines as $childOutline) {
+            if ($lastChild === null) {
+                $lastChild = $childOutline->dumpOutline($factory, true, $outlineDictionary);
+                $outlineDictionary->First = $lastChild;
+            } else {
+                $childOutlineDictionary = $childOutline->dumpOutline($factory, true, $outlineDictionary, $lastChild);
+                $lastChild->Next = $childOutlineDictionary;
+                $lastChild       = $childOutlineDictionary;
+            }
+        }
+        $outlineDictionary->Last = $lastChild;
+
+        if (count($this->childOutlines) != 0) {
+            $outlineDictionary->Count = new Zend_Pdf_Element_Numeric(($this->isOpen()? 1 : -1)*count($this->childOutlines));
+        }
+
+        return $outlineDictionary;
+    }
+}

+ 439 - 0
library/Zend/Pdf/Outline/Loaded.php

@@ -0,0 +1,439 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** Zend_Pdf_ElementFactory */
+require_once 'Zend/Pdf/ElementFactory.php';
+
+/** Zend_Pdf_Outline */
+require_once 'Zend/Pdf/Outline.php';
+
+/**
+ * Traceable PDF outline representation class
+ *
+ * Instances of this class trace object update uperations. That allows to avoid outlines PDF tree update
+ * which should be performed at each document update otherwise.
+ *
+ * @package    Zend_Pdf
+ * @subpackage Outlines
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Pdf_Outline_Loaded extends Zend_Pdf_Outline
+{
+    /**
+     * Outline dictionary object
+     *
+     * @var Zend_Pdf_Element_Dictionary|Zend_Pdf_Element_Object|Zend_Pdf_Element_Reference
+     */
+    protected $_outlineDictionary;
+
+	/**
+     * original array of child outlines
+     *
+     * @var array
+     */
+	protected $_originalChildOutlines = array();
+
+    /**
+     * Get outline title.
+     *
+     * @return string
+     * @throws Zend_Pdf_Exception
+     */
+    public function getTitle()
+    {
+    	if ($this->_outlineDictionary->Title === null) {
+    		require_once 'Zend/Pdf/Exception.php';
+    		throw new Zend_Pdf_Exception('Outline dictionary Title entry is required.');
+    	}
+        return $this->_outlineDictionary->Title->value;
+    }
+
+    /**
+     * Set outline title
+     *
+     * @param string $title
+     * @return Zend_Pdf_Outline
+     */
+    public function setTitle($title)
+    {
+    	$this->_outlineDictionary->Title->touch();
+        $this->_outlineDictionary->Title = new Zend_Pdf_Element_String($title);
+        return $this;
+    }
+
+    /**
+     * Sets 'isOpen' outline flag
+     *
+     * @param boolean $isOpen
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsOpen($isOpen)
+    {
+    	parent::setIsOpen($isOpen);
+
+    	if ($this->_outlineDictionary->Count === null) {
+            // Do Nothing.
+            return this;
+        }
+
+        $childrenCount = $this->_outlineDictionary->Count->value;
+        $isOpenCurrentState = ($childrenCount > 0);
+        if ($isOpen != $isOpenCurrentState) {
+        	$this->_outlineDictionary->Count->touch();
+        	$this->_outlineDictionary->Count->value = ($isOpen? 1 : -1)*abs($childrenCount);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns true if outline item is displayed in italic
+     *
+     * @return boolean
+     */
+    public function isItalic()
+    {
+        if ($this->_outlineDictionary->F === null) {
+            return false;
+        }
+        return $this->_outlineDictionary->F->value & 1;
+    }
+
+    /**
+     * Sets 'isItalic' outline flag
+     *
+     * @param boolean $isItalic
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsItalic($isItalic)
+    {
+    	if ($this->_outlineDictionary->F === null) {
+    		$this->_outlineDictionary->touch();
+    		$this->_outlineDictionary->F = new Zend_Pdf_Element_Numeric($isItalic? 1 : 0);
+    	} else {
+    		$this->_outlineDictionary->F->touch();
+    		if ($isItalic) {
+    			$this->_outlineDictionary->F->value = $this->_outlineDictionary->F->value | 1;
+    		} else {
+    			$this->_outlineDictionary->F->value = $this->_outlineDictionary->F->value | ~1;
+    		}
+    	}
+        return $this;
+    }
+
+    /**
+     * Returns true if outline item is displayed in bold
+     *
+     * @return boolean
+     */
+    public function isBold()
+    {
+        if ($this->_outlineDictionary->F === null) {
+            return false;
+        }
+        return $this->_outlineDictionary->F->value & 2;
+    }
+
+    /**
+     * Sets 'isBold' outline flag
+     *
+     * @param boolean $isBold
+     * @return Zend_Pdf_Outline
+     */
+    public function setIsBold($isBold)
+    {
+        if ($this->_outlineDictionary->F === null) {
+            $this->_outlineDictionary->touch();
+            $this->_outlineDictionary->F = new Zend_Pdf_Element_Numeric($isBold? 2 : 0);
+        } else {
+            $this->_outlineDictionary->F->touch();
+            if ($isBold) {
+                $this->_outlineDictionary->F->value = $this->_outlineDictionary->F->value | 2;
+            } else {
+                $this->_outlineDictionary->F->value = $this->_outlineDictionary->F->value | ~2;
+            }
+        }
+        return $this;
+    }
+
+
+    /**
+     * Get outline text color.
+     *
+     * @return Zend_Pdf_Color_Rgb
+     */
+    public function getColor()
+    {
+        if ($this->_outlineDictionary->C === null) {
+        	return null;
+        }
+
+        $components = $this->_outlineDictionary->C->items;
+
+        require_once 'Zend/Pdf/Color/Rgb.php';
+    	return new Zend_Pdf_Color_Rgb($components[0], $components[1], $components[2]);
+    }
+
+    /**
+     * Set outline text color.
+     * (null means default color which is black)
+     *
+     * @param Zend_Pdf_Color_Rgb $color
+     * @return Zend_Pdf_Outline
+     */
+    public function setColor(Zend_Pdf_Color_Rgb $color)
+    {
+    	$this->_outlineDictionary->touch();
+
+    	if ($color === null) {
+    		$this->_outlineDictionary->C = null;
+    	} else {
+    		$components = $color->getComponents();
+    		$colorComponentElements = array(new Zend_Pdf_Element_Numeric($components[0]),
+    		                                new Zend_Pdf_Element_Numeric($components[1]),
+    		                                new Zend_Pdf_Element_Numeric($components[2]));
+    		$this->_outlineDictionary->C = new Zend_Pdf_Element_Array($colorComponentElements);
+    	}
+
+    	return $this;
+    }
+
+    /**
+     * Get outline target.
+     *
+     * @return Zend_Pdf_Destination|Zend_Pdf_Action|string
+     * @thows Zend_Pdf_Exception
+     */
+    public function getTarget()
+    {
+        if ($this->_outlineDictionary->Dest !== null) {
+            if ($this->_outlineDictionary->A !== null) {
+                require_once 'Zend/Pdf/Exception.php';
+                throw new Zend_Pdf_Exception('Outline dictionary may contain Dest or A entry, but not both.');
+            }
+
+            if ($this->_outlineDictionary->Dest->getType() == Zend_Pdf_Element::TYPE_NAME  ||
+                $this->_outlineDictionary->Dest->getType() == Zend_Pdf_Element::TYPE_STRING) {
+                // It's a named destination
+                /** @todo return named destination object */
+                return $this->_outlineDictionary->Dest->value;
+            } else {
+                return Zend_Pdf_Destination::load($this->_outlineDictionary->Dest);
+            }
+        } else if ($this->_outlineDictionary->A !== null) {
+            return Zend_Pdf_Action::load($this->_outlineDictionary->A);
+        }
+
+        return null;
+    }
+
+    /**
+     * Set outline target.
+     * Null means no target
+     *
+     * @param Zend_Pdf_Destination|Zend_Pdf_Action|string $target
+     * @return Zend_Pdf_Outline
+     * @throws Zend_Pdf_Exception
+     */
+    public function setTarget($target)
+    {
+    	$this->_outlineDictionary->touch();
+
+        if ($target === null) {
+            $this->_outlineDictionary->Dest = null;
+            $this->_outlineDictionary->A    = null;
+        } else if (is_string($target)) {
+            $this->_outlineDictionary->Dest = new Zend_Pdf_Element_String($target);
+            $this->_outlineDictionary->A    = null;
+        } else if ($target instanceof Zend_Pdf_Destination) {
+            $this->_outlineDictionary->Dest = $target->getDestinationArray()->items[0];
+            $this->_outlineDictionary->A    = null;
+        } else if ($target instanceof Zend_Pdf_Action) {
+            $this->_outlineDictionary->Dest = null;
+            $this->_outlineDictionary->A    = $target->getDictionary();
+        } else {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception('Outline target has to be Zend_Pdf_Destination or Zend_Pdf_Action object, string or null');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set outline options
+     *
+     * @param array $options
+     * @return Zend_Pdf_Actions_Traceable
+     * @throws Zend_Pdf_Exception
+     */
+    public function setOptions(array $options)
+    {
+    	parent::setOptions($options);
+
+        return $this;
+    }
+
+
+
+	/**
+	 * Create PDF outline object using specified dictionary
+	 *
+	 * @internal
+     * @param Zend_Pdf_Element $dictionary (It's actually Dictionary or Dictionary Object or Reference to a Dictionary Object)
+     * @param Zend_Pdf_Action  $parentAction
+     * @param SplObjectStorage $processedOutlines  List of already processed Outline dictionaries,
+     *                                             used to avoid cyclic references
+	 * @return Zend_Pdf_Action
+	 * @throws Zend_Pdf_Exception
+	 */
+	public function __construct(Zend_Pdf_Element $dictionary, SplObjectStorage $processedDictionaries = null)
+	{
+        if ($dictionary->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception('$dictionary mast be an indirect dictionary object.');
+        }
+
+        if ($processedDictionaries === null) {
+            $processedDictionaries = new SplObjectStorage();
+        }
+        $processedDictionaries->attach($dictionary);
+
+        $this->_outlineDictionary = $dictionary;
+
+        if ($dictionary->Count !== null) {
+            if ($dictionary->Count->getType() != Zend_Pdf_Element::TYPE_NUMERIC) {
+                require_once 'Zend/Pdf/Exception.php';
+                throw new Zend_Pdf_Exception('Outline dictionary Count entry must be a numeric element.');
+            }
+
+            $childOutlinesCount = $dictionary->Count->value;
+            if ($childOutlinesCount > 0) {
+            	$this->_open = true;
+            }
+            $childOutlinesCount = abs($childOutlinesCount);
+
+            $childDictionary = $dictionary->First;
+            for ($count = 0; $count < $childOutlinesCount; $count++) {
+            	if ($childDictionary === null) {
+            		require_once 'Zend/Pdf/Exception.php';
+                    throw new Zend_Pdf_Exception('Outline childs load error.');
+            	}
+
+            	if (!$processedDictionaries->contains($childDictionary)) {
+                    $this->childOutlines[] = new Zend_Pdf_Outline_Loaded($childDictionary, $processedDictionaries);
+            	}
+
+            	$childDictionary = $childDictionary->Next;
+            }
+
+            if ($childDictionary !== null) {
+                require_once 'Zend/Pdf/Exception.php';
+                throw new Zend_Pdf_Exception('Outline childs load error.');
+            }
+
+            $this->_originalChildOutlines = $this->childOutlines;
+        }
+	}
+
+    /**
+     * Dump Outline and it's child outlines into PDF structures
+     *
+     * Returns dictionary indirect object or reference
+     *
+     * @internal
+     * @param Zend_Pdf_ElementFactory    $factory object factory for newly created indirect objects
+     * @param boolean $updateNavigation  Update navigation flag
+     * @param Zend_Pdf_Element $parent   Parent outline dictionary reference
+     * @param Zend_Pdf_Element $prev     Previous outline dictionary reference
+     * @return Zend_Pdf_Element
+     */
+    public function dumpOutline(Zend_Pdf_ElementFactory_Interface $factory, $updateNavigation, Zend_Pdf_Element $parent, Zend_Pdf_Element $prev = null)
+    {
+    	if ($updateNavigation) {
+    		$this->_outlineDictionary->touch();
+
+    		$this->_outlineDictionary->Parent = $parent;
+    		$this->_outlineDictionary->Prev   = $prev;
+    		$this->_outlineDictionary->Next   = null;
+    	}
+
+    	$updateChildNavigation = false;
+    	if (count($this->_originalChildOutlines) != count($this->childOutlines)) {
+    		// If original and current children arrays have different size then children list was updated
+    		$updateChildNavigation = true;
+    	} else if ( !(array_keys($this->_originalChildOutlines) === array_keys($this->childOutlines)) ) {
+    		// If original and current children arrays have different keys (with a glance to an order) then children list was updated
+            $updateChildNavigation = true;
+    	} else {
+    		foreach ($this->childOutlines as $key => $childOutline) {
+    			if ($this->_originalChildOutlines[$key] !== $childOutline) {
+    				$updateChildNavigation = true;
+    			}
+    		}
+    	}
+
+    	$lastChild = null;
+    	if ($updateChildNavigation) {
+    		$this->_outlineDictionary->touch();
+    		$this->_outlineDictionary->First = null;
+
+    		foreach ($this->childOutlines as $childOutline) {
+    			if ($lastChild === null) {
+                    // First pass. Update Outlines dictionary First entry using corresponding value
+                    $lastChild = $childOutline->dumpOutline($factory, $updateChildNavigation, $this->_outlineDictionary);
+                    $this->_outlineDictionary->First = $lastChild;
+    			} else {
+                    // Update previous outline dictionary Next entry (Prev is updated within dumpOutline() method)
+    				$childOutlineDictionary = $childOutline->dumpOutline($factory, $updateChildNavigation, $this->_outlineDictionary, $lastChild);
+                    $lastChild->Next = $childOutlineDictionary;
+                    $lastChild       = $childOutlineDictionary;
+    			}
+    		}
+
+    		$this->_outlineDictionary->Last  = $lastChild;
+
+            if (count($this->childOutlines) != 0) {
+                $this->_outlineDictionary->Count = new Zend_Pdf_Element_Numeric(($this->isOpen()? 1 : -1)*count($this->childOutlines));
+            } else {
+            	$this->_outlineDictionary->Count = null;
+            }
+        } else {
+            foreach ($this->childOutlines as $childOutline) {
+                $lastChild = $childOutline->dumpOutline($factory, $updateChildNavigation, $this->_outlineDictionary, $lastChild);
+            }
+        }
+
+    	return $this->_outlineDictionary;
+    }
+
+    public function dump($level = 0)
+    {
+        printf(":%3d:%s:%s:%s%s  :\n", count($this->childOutlines),$this->isItalic()? 'i':' ', $this->isBold()? 'b':' ', str_pad('', 4*$level), $this->getTitle());
+
+        if ($this->isOpen()  ||  true) {
+            foreach ($this->childOutlines as $child) {
+                $child->dump($level + 1);
+            }
+        }
+    }
+}

+ 64 - 0
library/Zend/Pdf/Target.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** Zend_Pdf_ElementFactory */
+require_once 'Zend/Pdf/ElementFactory.php';
+
+
+/**
+ * PDF target (action or destination)
+ *
+ * @package    Zend_Pdf
+ * @subpackage Actions
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Pdf_Target
+{
+    /**
+     * Parse resource and return it as an Action or Explicit Destination
+     *
+     * $param Zend_Pdf_Element $resource
+     * @return Zend_Pdf_Destination|
+     * @throws Zend_Pdf_Exception
+     */
+    public static function load(Zend_Pdf_Element $resource) {
+        if ($resource->getType() == Zend_Pdf_Element::TYPE_ARRAY) {
+            // Resource is an array, just treat it as an explicit destination array
+            return Zend_Pdf_Destination::load($resource);
+        } else if ($resource->getType() == Zend_Pdf_Element::TYPE_DICTIONARY) {
+            // Load destination as appropriate action
+            return Zend_Pdf_Action::load($resource);
+        } else {
+            require_once 'Zend/Pdf/Exception.php';
+            throw new Zend_Pdf_Exception( 'PDF named destination entry must be an array or dictionary.' );
+        }
+    }
+
+    /**
+     * Get resource
+     *
+     * @internal
+     * @return Zend_Pdf_Element
+     */
+    abstract public function getResource();
+}

+ 4 - 4
tests/Zend/Pdf/ActionTest.php

@@ -274,7 +274,7 @@ class Zend_Pdf_ActionTest extends PHPUnit_Framework_TestCase
         $root->rebuildSubtree();
 
         $this->assertEquals(
-            $root->getDictionary()->toString(),
+            $root->getResource()->toString(),
             '<</Type /Action /S /GoToR '
             . "/Next [<</Type /Action /S /GoToE >> <</Type /Action /S /Launch >> <</Type /Action /S /URI >> <</Type /Action /S /Sound >> <</Type /Action /S /Movie >> \n"
             .        "<</Type /Action /S /Hide >> <</Type /Action /S /Named >> <</Type /Action /S /SubmitForm >> <</Type /Action /S /ResetForm >> <</Type /Action /S /ImportData >> \n"
@@ -316,7 +316,7 @@ class Zend_Pdf_ActionTest extends PHPUnit_Framework_TestCase
         $action1->rebuildSubtree();
 
         $this->assertEquals(
-            $action1->getDictionary()->toString(),
+            $action1->getResource()->toString(),
             '<</Type /Action /S /GoTo /D (Destination 1) '
             . '/Next <</Type /Action /S /Thread /D (Destination 2) '
             .         '/Next [<</Type /Action /S /GoTo /D (Destination 3) >> <</Type /Action /S /GoToR >> ] >> >>');
@@ -331,7 +331,7 @@ class Zend_Pdf_ActionTest extends PHPUnit_Framework_TestCase
 
     	$action1->rebuildSubtree();
 
-    	$this->assertEquals($action1->getDictionary()->toString(),
+    	$this->assertEquals($action1->getResource()->toString(),
     	                    '<</Type /Action /S /GoTo /D (SomeNamedDestination) /Next <</Type /Action /S /GoTo /D (AnotherNamedDestination) >> >>');
 
     	$action1->clean();
@@ -349,7 +349,7 @@ class Zend_Pdf_ActionTest extends PHPUnit_Framework_TestCase
         $action = Zend_Pdf_Action_GoTo::create($destination);
         $action->rebuildSubtree();
 
-        $this->assertEquals($action->getDictionary()->toString(),
+        $this->assertEquals($action->getResource()->toString(),
                             '<</Type /Action /S /GoTo /D [4 0 R /Fit ] >>');
 
         $action->clean();

+ 16 - 16
tests/Zend/Pdf/DestinationTest.php

@@ -48,7 +48,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_Zoom);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /XYZ 0 842 1 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /XYZ 0 842 1 ]');
 
 
         // Zend_Pdf_Destination_Fit
@@ -59,7 +59,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_Fit);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /Fit ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /Fit ]');
 
 
         // Zend_Pdf_Destination_FitHorizontally
@@ -71,7 +71,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitHorizontally);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitH 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitH 842 ]');
 
 
         // Zend_Pdf_Destination_FitVertically
@@ -83,7 +83,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitVertically);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitV 0 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitV 0 ]');
 
 
         // Zend_Pdf_Destination_FitRectangle
@@ -98,7 +98,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitRectangle);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitR 0 10 595 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitR 0 10 595 842 ]');
 
 
         // Zend_Pdf_Destination_FitBoundingBox
@@ -109,7 +109,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBox);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitB ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitB ]');
 
 
         // Zend_Pdf_Destination_FitBoundingBoxHorizontally
@@ -121,7 +121,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBoxHorizontally);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitBH 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitBH 842 ]');
 
 
         // Zend_Pdf_Destination_FitBoundingBoxVertically
@@ -133,7 +133,7 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
         $destination = Zend_Pdf_Destination::load($destArray);
 
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBoxVertically);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitBV 0 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitBV 0 ]');
     }
 
     public function testGettersSetters()
@@ -254,34 +254,34 @@ class Zend_Pdf_DestinationTest extends PHPUnit_Framework_TestCase
 
         $destination = Zend_Pdf_Destination_Zoom::create($page2, 0, 842, 0.5);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_Zoom);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /XYZ 0 842 0.5 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /XYZ 0 842 0.5 ]');
 
         $destination = Zend_Pdf_Destination_Fit::create($page2->getPageDictionary());
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_Fit);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /Fit ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /Fit ]');
 
         $destination = Zend_Pdf_Destination_FitHorizontally::create($page2->getPageDictionary(), 842);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitHorizontally);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitH 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitH 842 ]');
 
         $destination = Zend_Pdf_Destination_FitVertically::create(2, 0);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitVertically);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[2 /FitV 0 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[2 /FitV 0 ]');
 
         $destination = Zend_Pdf_Destination_FitRectangle::create($page1, 0, 10, 595, 842);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitRectangle);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[3 0 R /FitR 0 10 595 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[3 0 R /FitR 0 10 595 842 ]');
 
         $destination = Zend_Pdf_Destination_FitBoundingBox::create(1);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBox);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[1 /FitB ]');
+        $this->assertEquals($destination->getResource()->toString(), '[1 /FitB ]');
 
         $destination = Zend_Pdf_Destination_FitBoundingBoxHorizontally::create($page2, 842);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBoxHorizontally);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitBH 842 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitBH 842 ]');
 
         $destination = Zend_Pdf_Destination_FitBoundingBoxVertically::create($page2, 0);
         $this->assertTrue($destination instanceof Zend_Pdf_Destination_FitBoundingBoxVertically);
-        $this->assertEquals($destination->getDestinationArray()->toString(), '[4 0 R /FitBV 0 ]');
+        $this->assertEquals($destination->getResource()->toString(), '[4 0 R /FitBV 0 ]');
     }
 }

+ 1 - 1
tests/Zend/Pdf/Element/ObjectTest.php

@@ -37,7 +37,7 @@ class Zend_Pdf_Element_ObjectTest extends PHPUnit_Framework_TestCase
         try {
             $obj2 = new Zend_Pdf_Element_Object($obj1, 1, 0, new Zend_Pdf_ElementFactory(1));
         } catch (Zend_Pdf_Exception $e) {
-            $this->assertRegExp('/must not be instance of Zend_Pdf_Element_Object/i', $e->getMessage());
+            $this->assertRegExp('/must not be an instance of Zend_Pdf_Element_Object/i', $e->getMessage());
             return;
         }
         $this->fail('Expected Zend_Pdf_Exception to be thrown');