Bladeren bron

Zend_Pdf: copy pages from one document into another. [ZF-449]

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@22797 44c647ce-9c0f-0410-b52a-842ac1e357ba
alexander 15 jaren geleden
bovenliggende
commit
226c6f92d1

+ 60 - 17
documentation/manual/en/module_specs/Zend_Pdf-Pages.xml

@@ -21,16 +21,9 @@
             objects directly or by calling the <methodname>Zend_Pdf::newPage()</methodname> method,
             which returns a <classname>Zend_Pdf_Page</classname> object.
             <methodname>Zend_Pdf::newPage()</methodname> creates a page that is already attached to
-            a document. Unattached pages can't be used with multiple <acronym>PDF</acronym>
-            documents, but they are somewhat more performant.
-
-            <footnote>
-                <para>
-                    It's a limitation of current Zend Framework version. It will be eliminated in
-                    future versions. But unattached pages will always give better (more optimal)
-                    result for sharing pages between documents.
-                </para>
-            </footnote>
+            a document. Attached pages can't be used with another <acronym>PDF</acronym>
+            documents until it's not cloned. See <link linkend="zend.pdf.pages.cloning">Page 
+            cloning</link> section for the details.
         </para>
 
         <para>
@@ -80,12 +73,12 @@ unset($pdf->pages[$id]);
         <title>Page cloning</title>
 
         <para>
-            Existing <acronym>PDF</acronym> page can be cloned by creating new
+            Existing <acronym>PDF</acronym> page can be duplicated by creating new
             <classname>Zend_Pdf_Page</classname> object with existing page as a parameter:
         </para>
 
         <example id="zend.pdf.pages.example-2">
-            <title>Cloning existing page</title>
+            <title>Duplicating existing page</title>
 
             <programlisting language="php"><![CDATA[
 ...
@@ -94,11 +87,13 @@ $template = $pdf->pages[$templatePageIndex];
 ...
 // Add new page
 $page1 = new Zend_Pdf_Page($template);
+$page1->drawText('Some text...', $x, $y);
 $pdf->pages[] = $page1;
 ...
 
 // Add another page
 $page2 = new Zend_Pdf_Page($template);
+$page2->drawText('Another text...', $x, $y);
 $pdf->pages[] = $page2;
 ...
 
@@ -108,18 +103,66 @@ unset($pdf->pages[$templatePageIndex]);
 ...
 ]]></programlisting>
         </example>
-
+        
         <para>
-            It's useful if you need several pages to be created using one template.
+            It's useful if you need several pages to be created using one template. 
         </para>
 
         <caution>
             <para>
-                Important! Cloned page shares some <acronym>PDF</acronym> resources with a template
-                page, so it can be used only within the same document as a template page. Modified
-                document can be saved as new one.
+                Important! Duplicated page shares some <acronym>PDF</acronym> resources with 
+                a template page, so it can be used only within the same document as a template page. 
+                Modified document can be saved as new one.
             </para>
         </caution>
+
+        <para>
+            <code>clone</code> operator may be used to create page which is not attached to any document.
+            It takes more time than duplicating page since it needs to copy all dependent objects
+            (used fonts, images and other resources), but it allows to use pages from different source
+            documents to create new one: 
+        </para>
+
+        <example id="zend.pdf.pages.example-3">
+            <title>Cloning existing page</title>
+
+            <programlisting language="php"><![CDATA[
+$page1 = clone $pdf1->pages[$templatePageIndex1];
+$page2 = clone $pdf2->pages[$templatePageIndex2];
+$page1->drawText('Some text...', $x, $y);
+$page2->drawText('Another text...', $x, $y);
+...
+$pdf = new Zend_Pdf();
+$pdf->pages[] = $page1;
+$pdf->pages[] = $page2;
+]]></programlisting>
+        </example>
+        
+        <para>
+            If several template pages are planned to be used as templates then it could be more efficient 
+            to utilize <classname>Zend_Pdf_Resource_Extractor</classname> class which gives an ability 
+            to share resources between cloned pages - fonts, images, etc. (otherwise new resource copy 
+            will be created for each cloned page):  
+        </para>
+
+        <example id="zend.pdf.pages.example-4">
+            <title>
+                Cloning existing page using <classname>Zend_Pdf_Resource_Extractor</classname> class
+            </title>
+
+            <programlisting language="php"><![CDATA[
+$extractor = new Zend_Pdf_Resource_Extractor();
+....
+$page1 = $extractor->clonePage($pdf->pages[$templatePageIndex1]);
+$page2 = $extractor->clonePage($pdf->pages[$templatePageIndex2]);
+$page1->drawText('Some text...', $x, $y);
+$page2->drawText('Another text...', $x, $y);
+...
+$pdf = new Zend_Pdf();
+$pdf->pages[] = $page1;
+$pdf->pages[] = $page2;
+]]></programlisting>
+        </example>
     </sect2>
 </sect1>
 <!--

+ 3 - 0
library/Zend/Pdf.php

@@ -47,6 +47,9 @@ require_once 'Zend/Pdf/Image.php';
 /** Zend_Pdf_Font */
 require_once 'Zend/Pdf/Font.php';
 
+/** Zend_Pdf_Resource_Extractor */
+require_once 'Zend/Pdf/Resource/Extractor.php';
+
 
 /** Internally used classes */
 require_once 'Zend/Pdf/Element.php';

+ 18 - 0
library/Zend/Pdf/Element.php

@@ -64,6 +64,24 @@ abstract class Zend_Pdf_Element
      */
     abstract public function toString($factory = null);
 
+    const CLONE_MODE_SKIP_PAGES    = 1; // Do not follow pages during deep copy process
+    const CLONE_MODE_FORCE_CLONING = 2; // Force top level object cloning even it's already processed
+
+    /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @todo It's nevessry to check if SplObjectStorage class works faster
+     * (Needs PHP 5.3.x to attach object _with_ additional data to storage)
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        return clone $this;
+    }
 
     /**
      * Set top level parent indirect object.

+ 33 - 0
library/Zend/Pdf/Element/Array.php

@@ -129,6 +129,39 @@ class Zend_Pdf_Element_Array extends Zend_Pdf_Element
     }
 
     /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        $newArray = new self();
+
+        foreach ($this->items as $key => $value) {
+            $newArray->items[$key] = $value->makeClone($factory, $processed, $mode);
+        }
+
+        return $newArray;
+    }
+
+    /**
+     * Set top level parent indirect object.
+     *
+     * @param Zend_Pdf_Element_Object $parent
+     */
+    public function setParentObject(Zend_Pdf_Element_Object $parent)
+    {
+        parent::setParentObject($parent);
+
+        foreach ($this->items as $item) {
+            $item->setParentObject($parent);
+        }
+    }
+
+    /**
      * Convert PDF element to PHP type.
      *
      * Dictionary is returned as an associative array

+ 47 - 0
library/Zend/Pdf/Element/Dictionary.php

@@ -168,6 +168,53 @@ class Zend_Pdf_Element_Dictionary extends Zend_Pdf_Element
         return $outStr;
     }
 
+    /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     * @throws Zend_Pdf_Exception
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        if (isset($this->_items['Type'])) {
+            if ($this->_items['Type']->value == 'Pages') {
+                // It's a page tree node
+                // skip it and its children
+                return new Zend_Pdf_Element_Null();
+            }
+
+            if ($this->_items['Type']->value == 'Page'  &&
+                $mode == Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES
+            ) {
+                // It's a page node, skip it
+                return new Zend_Pdf_Element_Null();
+            }
+        }
+
+        $newDictionary = new self();
+        foreach ($this->_items as $key => $value) {
+            $newDictionary->_items[$key] = $value->makeClone($factory, $processed, $mode);
+        }
+
+        return $newDictionary;
+    }
+
+    /**
+     * Set top level parent indirect object.
+     *
+     * @param Zend_Pdf_Element_Object $parent
+     */
+    public function setParentObject(Zend_Pdf_Element_Object $parent)
+    {
+        parent::setParentObject($parent);
+
+        foreach ($this->_items as $item) {
+            $item->setParentObject($parent);
+        }
+    }
 
     /**
      * Convert PDF element to PHP type.

+ 44 - 0
library/Zend/Pdf/Element/Object.php

@@ -211,6 +211,40 @@ class Zend_Pdf_Element_Object extends Zend_Pdf_Element
         return call_user_func_array(array($this->_value, $method), $args);
     }
 
+    /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        $id = spl_object_hash($this);
+        if (isset($processed[$id])) {
+            // Do nothing if object is already processed
+            // return it
+            return $processed[$id];
+        }
+
+        // Create obect with null value and register it in $processed container
+        $processed[$id] = $clonedObject = $factory->newObject(new Zend_Pdf_Element_Null());
+
+        // Pecursively process actual data
+        $clonedObject->_value = $this->_value->makeClone($factory, $processed, $mode);
+
+        if ($clonedObject->_value instanceof Zend_Pdf_Element_Null) {
+            // Do not store null objects within $processed container since it may be filtered
+            // by $mode parameter but used in some future pass
+            unset($processed[$id]);
+
+            // Return direct null object
+            return $clonedObject->_value;
+        }
+
+        return $clonedObject;
+    }
 
     /**
      * Mark object as modified, to include it into new PDF file segment
@@ -231,6 +265,16 @@ class Zend_Pdf_Element_Object extends Zend_Pdf_Element
     }
 
     /**
+     * Return direct object
+     *
+     * @return Zend_Pdf_Element_Object
+     */
+    public function getElement()
+    {
+        return $this->_value;
+    }
+
+    /**
      * Clean up resources, used by object
      */
     public function cleanUp()

+ 76 - 45
library/Zend/Pdf/Element/Object/Stream.php

@@ -57,13 +57,13 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
 
     /**
      * Stored original stream object dictionary.
-     * Used to decode stream during an access time.
+     * Used to decode stream at access time.
      *
-     * The only properties, which affect decoding, are sored here.
+     * The only properties affecting decoding are sored here.
      *
      * @var array|null
      */
-    private $_originalDictionary = null;
+    private $_initialDictionaryData = null;
 
     /**
      * Object constructor
@@ -91,77 +91,80 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
 
 
     /**
-     * Store original dictionary information in $_originalDictionary class member.
-     * Used to store information and to normalize filters information before defiltering.
+     * Extract dictionary data which are used to store information and to normalize filters
+     * information before defiltering.
      *
+     * @return array
      */
-    private function _storeOriginalDictionary()
+    private function _extractDictionaryData()
     {
-        $this->_originalDictionary = array();
+        $dictionaryArray = array();
 
-        $this->_originalDictionary['Filter']      = array();
-        $this->_originalDictionary['DecodeParms'] = array();
+        $dictionaryArray['Filter']      = array();
+        $dictionaryArray['DecodeParms'] = array();
         if ($this->_dictionary->Filter === null) {
             // Do nothing.
         } else if ($this->_dictionary->Filter->getType() == Zend_Pdf_Element::TYPE_ARRAY) {
             foreach ($this->_dictionary->Filter->items as $id => $filter) {
-                $this->_originalDictionary['Filter'][$id]      = $filter->value;
-                $this->_originalDictionary['DecodeParms'][$id] = array();
+                $dictionaryArray['Filter'][$id]      = $filter->value;
+                $dictionaryArray['DecodeParms'][$id] = array();
 
                 if ($this->_dictionary->DecodeParms !== null ) {
                     if ($this->_dictionary->DecodeParms->items[$id] !== null &&
                         $this->_dictionary->DecodeParms->items[$id]->value !== null ) {
                         foreach ($this->_dictionary->DecodeParms->items[$id]->getKeys() as $paramKey) {
-                            $this->_originalDictionary['DecodeParms'][$id][$paramKey] =
+                            $dictionaryArray['DecodeParms'][$id][$paramKey] =
                                   $this->_dictionary->DecodeParms->items[$id]->$paramKey->value;
                         }
                     }
                 }
             }
         } else if ($this->_dictionary->Filter->getType() != Zend_Pdf_Element::TYPE_NULL) {
-            $this->_originalDictionary['Filter'][0]      = $this->_dictionary->Filter->value;
-            $this->_originalDictionary['DecodeParms'][0] = array();
+            $dictionaryArray['Filter'][0]      = $this->_dictionary->Filter->value;
+            $dictionaryArray['DecodeParms'][0] = array();
             if ($this->_dictionary->DecodeParms !== null ) {
                 foreach ($this->_dictionary->DecodeParms->getKeys() as $paramKey) {
-                    $this->_originalDictionary['DecodeParms'][0][$paramKey] =
+                    $dictionaryArray['DecodeParms'][0][$paramKey] =
                           $this->_dictionary->DecodeParms->$paramKey->value;
                 }
             }
         }
 
         if ($this->_dictionary->F !== null) {
-            $this->_originalDictionary['F'] = $this->_dictionary->F->value;
+            $dictionaryArray['F'] = $this->_dictionary->F->value;
         }
 
-        $this->_originalDictionary['FFilter']      = array();
-        $this->_originalDictionary['FDecodeParms'] = array();
+        $dictionaryArray['FFilter']      = array();
+        $dictionaryArray['FDecodeParms'] = array();
         if ($this->_dictionary->FFilter === null) {
             // Do nothing.
         } else if ($this->_dictionary->FFilter->getType() == Zend_Pdf_Element::TYPE_ARRAY) {
             foreach ($this->_dictionary->FFilter->items as $id => $filter) {
-                $this->_originalDictionary['FFilter'][$id]      = $filter->value;
-                $this->_originalDictionary['FDecodeParms'][$id] = array();
+                $dictionaryArray['FFilter'][$id]      = $filter->value;
+                $dictionaryArray['FDecodeParms'][$id] = array();
 
                 if ($this->_dictionary->FDecodeParms !== null ) {
                     if ($this->_dictionary->FDecodeParms->items[$id] !== null &&
                         $this->_dictionary->FDecodeParms->items[$id]->value !== null) {
                         foreach ($this->_dictionary->FDecodeParms->items[$id]->getKeys() as $paramKey) {
-                            $this->_originalDictionary['FDecodeParms'][$id][$paramKey] =
+                            $dictionaryArray['FDecodeParms'][$id][$paramKey] =
                                   $this->_dictionary->FDecodeParms->items[$id]->items[$paramKey]->value;
                         }
                     }
                 }
             }
         } else {
-            $this->_originalDictionary['FFilter'][0]      = $this->_dictionary->FFilter->value;
-            $this->_originalDictionary['FDecodeParms'][0] = array();
+            $dictionaryArray['FFilter'][0]      = $this->_dictionary->FFilter->value;
+            $dictionaryArray['FDecodeParms'][0] = array();
             if ($this->_dictionary->FDecodeParms !== null ) {
                 foreach ($this->_dictionary->FDecodeParms->getKeys() as $paramKey) {
-                    $this->_originalDictionary['FDecodeParms'][0][$paramKey] =
+                    $dictionaryArray['FDecodeParms'][0][$paramKey] =
                           $this->_dictionary->FDecodeParms->items[$paramKey]->value;
                 }
             }
         }
+
+        return $dictionaryArray;
     }
 
     /**
@@ -171,21 +174,21 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
      */
     private function _decodeStream()
     {
-        if ($this->_originalDictionary === null) {
-            $this->_storeOriginalDictionary();
+        if ($this->_initialDictionaryData === null) {
+            $this->_initialDictionaryData = $this->_extractDictionaryData();
         }
 
         /**
          * All applied stream filters must be processed to decode stream.
          * If we don't recognize any of applied filetrs an exception should be thrown here
          */
-        if (isset($this->_originalDictionary['F'])) {
+        if (isset($this->_initialDictionaryData['F'])) {
             /** @todo Check, how external files can be processed. */
             require_once 'Zend/Pdf/Exception.php';
             throw new Zend_Pdf_Exception('External filters are not supported now.');
         }
 
-        foreach ($this->_originalDictionary['Filter'] as $id => $filterName ) {
+        foreach ($this->_initialDictionaryData['Filter'] as $id => $filterName ) {
             $valueRef = &$this->_value->value->getRef();
             $this->_value->value->touch();
             switch ($filterName) {
@@ -202,13 +205,13 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
                 case 'FlateDecode':
                     require_once 'Zend/Pdf/Filter/Compression/Flate.php';
                     $valueRef = Zend_Pdf_Filter_Compression_Flate::decode($valueRef,
-                                                                          $this->_originalDictionary['DecodeParms'][$id]);
+                                                                          $this->_initialDictionaryData['DecodeParms'][$id]);
                     break;
 
                 case 'LZWDecode':
                     require_once 'Zend/Pdf/Filter/Compression/Lzw.php';
                     $valueRef = Zend_Pdf_Filter_Compression_Lzw::decode($valueRef,
-                                                                        $this->_originalDictionary['DecodeParms'][$id]);
+                                                                        $this->_initialDictionaryData['DecodeParms'][$id]);
                     break;
 
                 case 'RunLengthDecode':
@@ -236,13 +239,13 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
          * All applied stream filters must be processed to encode stream.
          * If we don't recognize any of applied filetrs an exception should be thrown here
          */
-        if (isset($this->_originalDictionary['F'])) {
+        if (isset($this->_initialDictionaryData['F'])) {
             /** @todo Check, how external files can be processed. */
             require_once 'Zend/Pdf/Exception.php';
             throw new Zend_Pdf_Exception('External filters are not supported now.');
         }
 
-        $filters = array_reverse($this->_originalDictionary['Filter'], true);
+        $filters = array_reverse($this->_initialDictionaryData['Filter'], true);
 
         foreach ($filters as $id => $filterName ) {
             $valueRef = &$this->_value->value->getRef();
@@ -261,13 +264,13 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
                 case 'FlateDecode':
                     require_once 'Zend/Pdf/Filter/Compression/Flate.php';
                     $valueRef = Zend_Pdf_Filter_Compression_Flate::encode($valueRef,
-                                                                          $this->_originalDictionary['DecodeParms'][$id]);
+                                                                          $this->_initialDictionaryData['DecodeParms'][$id]);
                     break;
 
                 case 'LZWDecode':
                     require_once 'Zend/Pdf/Filter/Compression/Lzw.php';
                     $valueRef = Zend_Pdf_Filter_Compression_Lzw::encode($valueRef,
-                                                                        $this->_originalDictionary['DecodeParms'][$id]);
+                                                                        $this->_initialDictionaryData['DecodeParms'][$id]);
                     break;
 
                  case 'RunLengthDecode':
@@ -297,8 +300,8 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
             /**
              * If stream is note decoded yet, then store original decoding options (do it only once).
              */
-            if (( !$this->_streamDecoded ) && ($this->_originalDictionary === null)) {
-                $this->_storeOriginalDictionary();
+            if (( !$this->_streamDecoded ) && ($this->_initialDictionaryData === null)) {
+                $this->_initialDictionaryData = $this->_extractDictionaryData();
             }
 
             return $this->_dictionary;
@@ -374,6 +377,38 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
     }
 
     /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        $id = spl_object_hash($this);
+        if (isset($processed[$id])) {
+            // Do nothing if object is already processed
+            // return it
+            return $processed[$id];
+        }
+
+        $streamValue      = $this->_value;
+        $streamDictionary = $this->_dictionary->makeClone($factory, $processed, $mode);
+
+        // Make new empty instance of stream object and register it in $processed container
+        $processed[$id] = $clonedObject = $factory->newStreamObject('');
+
+        // Copy current object data and state
+        $clonedObject->_dictionary            = $this->_dictionary->makeClone($factory, $processed, $mode);
+        $clonedObject->_value                 = $this->_value->makeClone($factory, $processed, $mode);
+        $clonedObject->_initialDictionaryData = $this->_initialDictionaryData;
+        $clonedObject->_streamDecoded         = $this->_streamDecoded;
+
+        return  $clonedObject;
+    }
+
+    /**
      * Dump object to a string to save within PDF file
      *
      * $factory parameter defines operation context.
@@ -386,18 +421,14 @@ class Zend_Pdf_Element_Object_Stream extends Zend_Pdf_Element_Object
         $shift = $factory->getEnumerationShift($this->_factory);
 
         if ($this->_streamDecoded) {
-            $this->_storeOriginalDictionary();
+            $this->_initialDictionaryData = $this->_extractDictionaryData();
             $this->_encodeStream();
-        } else if ($this->_originalDictionary != null) {
-            $startDictionary = $this->_originalDictionary;
-            $this->_storeOriginalDictionary();
-            $newDictionary = $this->_originalDictionary;
+        } else if ($this->_initialDictionaryData != null) {
+            $newDictionary   = $this->_extractDictionaryData();
 
-            if ($startDictionary !== $newDictionary) {
-                $this->_originalDictionary = $startDictionary;
+            if ($this->_initialDictionaryData !== $newDictionary) {
                 $this->_decodeStream();
-
-                $this->_originalDictionary = $newDictionary;
+                $this->_initialDictionaryData = $newDictionary;
                 $this->_encodeStream();
             }
         }

+ 26 - 0
library/Zend/Pdf/Element/Reference.php

@@ -182,6 +182,32 @@ class Zend_Pdf_Element_Reference extends Zend_Pdf_Element
     }
 
     /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        if ($this->_ref === null) {
+            $this->_dereference();
+        }
+
+        // This code duplicates code in Zend_Pdf_Element_Object class,
+        // but allows to avoid unnecessary method call in most cases
+        $id = spl_object_hash($this->_ref);
+        if (isset($processed[$id])) {
+            // Do nothing if object is already processed
+            // return it
+            return $processed[$id];
+        }
+
+        return $this->_ref->makeClone($factory, $processed, $mode);
+    }
+
+    /**
      * Mark object as modified, to include it into new PDF file segment.
      */
     public function touch()

+ 13 - 0
library/Zend/Pdf/Element/Stream.php

@@ -105,6 +105,19 @@ class Zend_Pdf_Element_Stream extends Zend_Pdf_Element
 
 
     /**
+     * Detach PDF object from the factory (if applicable), clone it and attach to new factory.
+     *
+     * @param Zend_Pdf_ElementFactory $factory  The factory to attach
+     * @param array &$processed  List of already processed indirect objects, used to avoid objects duplication
+     * @param integer $mode  Cloning mode (defines filter for objects cloning)
+     * @returns Zend_Pdf_Element
+     */
+    public function makeClone(Zend_Pdf_ElementFactory $factory, array &$processed, $mode)
+    {
+        return new self($this->value->getRef());
+    }
+
+    /**
      * Return object as string
      *
      * @param Zend_Pdf_Factory $factory

+ 10 - 0
library/Zend/Pdf/ElementFactory.php

@@ -117,6 +117,16 @@ class Zend_Pdf_ElementFactory implements Zend_Pdf_ElementFactory_Interface
 
 
     /**
+     * Get factory
+     *
+     * @return Zend_Pdf_ElementFactory_Interface
+     */
+    public function getFactory()
+    {
+        return $this;
+    }
+
+    /**
      * Factory generator
      *
      * @param integer $objCount

+ 7 - 0
library/Zend/Pdf/ElementFactory/Interface.php

@@ -30,6 +30,13 @@
 interface Zend_Pdf_ElementFactory_Interface
 {
     /**
+     * Get factory
+     *
+     * @return Zend_Pdf_ElementFactory_Interface
+     */
+    public function getFactory();
+
+    /**
      * Close factory and clean-up resources
      *
      * @internal

+ 10 - 0
library/Zend/Pdf/ElementFactory/Proxy.php

@@ -57,6 +57,16 @@ class Zend_Pdf_ElementFactory_Proxy implements Zend_Pdf_ElementFactory_Interface
     }
 
     /**
+     * Get factory
+     *
+     * @return Zend_Pdf_ElementFactory_Interface
+     */
+    public function getFactory()
+    {
+        return $this->_factory->getFactory();
+    }
+
+    /**
      * Close factory and clean-up resources
      *
      * @internal

+ 82 - 36
library/Zend/Pdf/Page.php

@@ -185,7 +185,7 @@ class Zend_Pdf_Page
      *                   Zend_Pdf_ElementFactory_Interface $factory);
      * ---------------------------------------------------------
      *
-     * 2. Clone PDF page.
+     * 2. Make a copy of the PDF page.
      *    New page is created in the same context as source page. Object factory is shared.
      *    Thus it will be attached to the document, but need to be placed into Zend_Pdf::$pages array
      *    to be included into output.
@@ -215,18 +215,32 @@ class Zend_Pdf_Page
      */
     public function __construct($param1, $param2 = null, $param3 = null)
     {
-        if ($param1 instanceof Zend_Pdf_Element_Reference &&
-            $param1->getType() == Zend_Pdf_Element::TYPE_DICTIONARY &&
+        if (($param1 instanceof Zend_Pdf_Element_Reference ||
+             $param1 instanceof Zend_Pdf_Element_Object
+            ) &&
             $param2 instanceof Zend_Pdf_ElementFactory_Interface &&
             $param3 === null
            ) {
-            $this->_pageDictionary = $param1;
-            $this->_objFactory     = $param2;
-            $this->_attached       = true;
-            $this->_safeGS         = false;
+            switch ($param1->getType()) {
+                case Zend_Pdf_Element::TYPE_DICTIONARY:
+                    $this->_pageDictionary = $param1;
+                    $this->_objFactory     = $param2;
+                    $this->_attached       = true;
+                    $this->_safeGS         = false;
+                    return;
+                    break;
 
-            return;
+                case Zend_Pdf_Element::TYPE_NULL:
+                    $this->_objFactory = $param2;
+                    $pageWidth = $pageHeight = 0;
+                    break;
+
+                default:
+                    require_once 'Zend/Pdf/Exception.php';
+                    throw new Zend_Pdf_Exception('Unrecognized object type.');
+                    break;
 
+            }
         } else if ($param1 instanceof Zend_Pdf_Page && $param2 === null && $param3 === null) {
             // Clone existing page.
             // Let already existing content and resources to be shared between pages
@@ -337,17 +351,6 @@ class Zend_Pdf_Page
 
 
     /**
-     * Clone operator
-     *
-     * @throws Zend_Pdf_Exception
-     */
-    public function __clone()
-    {
-        require_once 'Zend/Pdf/Exception.php';
-        throw new Zend_Pdf_Exception('Cloning Zend_Pdf_Page object using \'clone\' keyword is not supported. Use \'new Zend_Pdf_Page($srcPage)\' syntax');
-    }
-
-    /**
      * Attach resource to the page
      *
      * @param string $type
@@ -409,6 +412,59 @@ class Zend_Pdf_Page
     }
 
     /**
+     * Clone page, extract it and dependent objects from the current document,
+     * so it can be used within other docs.
+     */
+    public function __clone()
+    {
+        $factory = Zend_Pdf_ElementFactory::createFactory(1);
+        $processed = array();
+
+        // Clone dictionary object.
+        // Do it explicitly to prevent sharing page attributes between different
+        // results of clonePage() operation (other resources are still shared)
+        $dictionary = new Zend_Pdf_Element_Dictionary();
+        foreach ($this->_pageDictionary->getKeys() as $key) {
+            $dictionary->$key = $this->_pageDictionary->$key->makeClone($factory->getFactory(),
+                                                                        $processed,
+                                                                        Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES);
+        }
+
+        $this->_pageDictionary = $factory->newObject($dictionary);
+        $this->_objFactory     = $factory;
+        $this->_attached       = false;
+        $this->_style          = null;
+        $this->_font           = null;
+    }
+
+    /**
+     * Clone page, extract it and dependent objects from the current document,
+     * so it can be used within other docs.
+     *
+     * @internal
+     * @param Zend_Pdf_ElementFactory_Interface $factory
+     * @param array $processed
+     * @return Zend_Pdf_Page
+     */
+    public function clonePage($factory, &$processed)
+    {
+        // Clone dictionary object.
+        // Do it explicitly to prevent sharing page attributes between different
+        // results of clonePage() operation (other resources are still shared)
+        $dictionary = new Zend_Pdf_Element_Dictionary();
+        foreach ($this->_pageDictionary->getKeys() as $key) {
+            $dictionary->$key = $this->_pageDictionary->$key->makeClone($factory->getFactory(),
+                                                                        $processed,
+                                                                        Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES);
+        }
+
+        $clonedPage = new Zend_Pdf_Page($factory->newObject($dictionary), $factory);
+        $clonedPage->_attached = false;
+
+        return $clonedPage;
+    }
+
+    /**
      * Retrive PDF file reference to the page
      *
      * @internal
@@ -497,22 +553,12 @@ class Zend_Pdf_Page
 
         if ($this->_attached) {
             require_once 'Zend/Pdf/Exception.php';
-            throw new Zend_Pdf_Exception('Page is attached to one documen, but rendered in context of another.');
-            /**
-             * @todo Page cloning must be implemented here instead of exception.
-             *       PDF objects (ex. fonts) can be shared between pages.
-             *       Thus all referenced objects, which can be modified, must be cloned recursively,
-             *       to avoid producing wrong object references in a context of source PDF.
-             */
-
-            //...
+            throw new Zend_Pdf_Exception('Page is attached to other documen. Use clone $page to get it context free.');
         } else {
             $objFactory->attach($this->_objFactory);
         }
     }
 
-
-
     /**
      * Set fill color.
      *
@@ -1425,7 +1471,7 @@ class Zend_Pdf_Page
      * @param integer $fillType
      * @return Zend_Pdf_Page
      */
-    public function drawRoundedRectangle($x1, $y1, $x2, $y2, $radius, 
+    public function drawRoundedRectangle($x1, $y1, $x2, $y2, $radius,
                                          $fillType = Zend_Pdf_Page::SHAPE_DRAW_FILL_AND_STROKE)
     {
 
@@ -1449,7 +1495,7 @@ class Zend_Pdf_Page
         $bottomRightY  = $y1;
         $bottomLeftX   = $x1;
         $bottomLeftY   = $y1;
-        
+
         //draw top side
         $x1Obj = new Zend_Pdf_Element_Numeric($topLeftX + $radius[0]);
         $y1Obj = new Zend_Pdf_Element_Numeric($topLeftY);
@@ -1459,7 +1505,7 @@ class Zend_Pdf_Page
         $this->_contents .= $x1Obj->toString() . ' ' . $y1Obj->toString() . " l\n";
 
         //draw top right corner if needed
-        if ($radius[1] != 0) {        
+        if ($radius[1] != 0) {
             $x1Obj = new Zend_Pdf_Element_Numeric($topRightX);
             $y1Obj = new Zend_Pdf_Element_Numeric($topRightY);
             $x2Obj = new Zend_Pdf_Element_Numeric($topRightX);
@@ -1478,7 +1524,7 @@ class Zend_Pdf_Page
         $this->_contents .= $x1Obj->toString() . ' ' . $y1Obj->toString() . " l\n";
 
         //draw bottom right corner if needed
-        if ($radius[2] != 0) {        
+        if ($radius[2] != 0) {
             $x1Obj = new Zend_Pdf_Element_Numeric($bottomRightX);
             $y1Obj = new Zend_Pdf_Element_Numeric($bottomRightY);
             $x2Obj = new Zend_Pdf_Element_Numeric($bottomRightX);
@@ -1497,7 +1543,7 @@ class Zend_Pdf_Page
         $this->_contents .= $x1Obj->toString() . ' ' . $y1Obj->toString() . " l\n";
 
         //draw bottom left corner if needed
-        if ($radius[3] != 0) {        
+        if ($radius[3] != 0) {
             $x1Obj = new Zend_Pdf_Element_Numeric($bottomLeftX);
             $y1Obj = new Zend_Pdf_Element_Numeric($bottomLeftY);
             $x2Obj = new Zend_Pdf_Element_Numeric($bottomLeftX);
@@ -1516,7 +1562,7 @@ class Zend_Pdf_Page
         $this->_contents .= $x1Obj->toString() . ' ' . $y1Obj->toString() . " l\n";
 
         //draw top left corner if needed
-        if ($radius[0] != 0) {        
+        if ($radius[0] != 0) {
             $x1Obj = new Zend_Pdf_Element_Numeric($topLeftX);
             $y1Obj = new Zend_Pdf_Element_Numeric($topLeftY);
             $x2Obj = new Zend_Pdf_Element_Numeric($topLeftX);

+ 55 - 0
library/Zend/Pdf/Resource.php

@@ -77,6 +77,61 @@ abstract class Zend_Pdf_Resource
     }
 
     /**
+     * Clone page, extract it and dependent objects from the current document,
+     * so it can be used within other docs.
+     */
+    public function __clone()
+    {
+        $factory = Zend_Pdf_ElementFactory::createFactory(1);
+        $processed = array();
+
+        CLONE_MODE_FORCE_CLONING
+
+        // Clone dictionary object.
+        // Do it explicitly to prevent sharing page attributes between different
+        // results of clonePage() operation (other resources are still shared)
+        $dictionary = new Zend_Pdf_Element_Dictionary();
+        foreach ($this->_pageDictionary->getKeys() as $key) {
+            $dictionary->$key = $this->_pageDictionary->$key->makeClone($factory->getFactory(),
+                                                                        $processed,
+                                                                        Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES);
+        }
+
+        $this->_pageDictionary = $factory->newObject($dictionary);
+        $this->_objFactory     = $factory;
+        $this->_attached       = false;
+        $this->_style          = null;
+        $this->_font           = null;
+    }
+
+    /**
+     * Clone page, extract it and dependent objects from the current document,
+     * so it can be used within other docs.
+     *
+     * @internal
+     * @param Zend_Pdf_ElementFactory_Interface $factory
+     * @param array $processed
+     * @return Zend_Pdf_Page
+     */
+    public function clonePage($factory, &$processed)
+    {
+        // Clone dictionary object.
+        // Do it explicitly to prevent sharing page attributes between different
+        // results of clonePage() operation (other resources are still shared)
+        $dictionary = new Zend_Pdf_Element_Dictionary();
+        foreach ($this->_pageDictionary->getKeys() as $key) {
+            $dictionary->$key = $this->_pageDictionary->$key->makeClone($factory->getFactory(),
+                                                                        $processed,
+                                                                        Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES);
+        }
+
+        $clonedPage = new Zend_Pdf_Page($factory->newObject($dictionary), $factory);
+        $clonedPage->_attached = false;
+
+        return $clonedPage;
+    }
+
+    /**
      * Get resource.
      * Used to reference resource in an internal PDF data structures (resource dictionaries)
      *

+ 86 - 0
library/Zend/Pdf/Resource/Extractor.php

@@ -0,0 +1,86 @@
+<?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
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id:
+ */
+
+/** Internally used classes */
+require_once 'Zend/Pdf/Element.php';
+require_once 'Zend/Pdf/Element/Array.php';
+require_once 'Zend/Pdf/Element/String/Binary.php';
+require_once 'Zend/Pdf/Element/Boolean.php';
+require_once 'Zend/Pdf/Element/Dictionary.php';
+require_once 'Zend/Pdf/Element/Name.php';
+require_once 'Zend/Pdf/Element/Null.php';
+require_once 'Zend/Pdf/Element/Numeric.php';
+require_once 'Zend/Pdf/Element/String.php';
+
+
+/**
+ * Resource extractor class is used to detach resources from original PDF document.
+ *
+ * It provides resources sharing, so different pages or other PDF resources can share
+ * its dependent resources (e.g. fonts or images) or other resources still use them without duplication.
+ * It also reduces output PDF size, required memory for PDF processing and
+ * processing time.
+ *
+ * The same extractor may be used for different source documents, several
+ * extractors may be used for constracting one target document, but extractor
+ * must not be shared between target documents.
+ *
+ * @package    Zend_Pdf
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Pdf_Resource_Extractor
+{
+    /**
+     * PDF objects factory.
+     *
+     * @var Zend_Pdf_ElementFactory_Interface
+     */
+    protected $_factory;
+
+    /**
+     * Reusable list of already processed objects
+     *
+     * @var array
+     */
+    protected $_processed;
+
+    /**
+     * Object constructor.
+     */
+    public function __construct()
+    {
+        $this->_factory   = Zend_Pdf_ElementFactory::createFactory(1);
+        $this->_processed = array();
+    }
+
+    /**
+     * Clone page, extract it and dependent objects from the current document,
+     * so it can be used within other docs
+     *
+     * return Zend_Pdf_Page
+     */
+    public function clonePage(Zend_Pdf_Page $page)
+    {
+        return $page->clonePage($this->_factory, $this->_processed);
+    }
+}
+