You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

773 lines
27 KiB

<?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-2014 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';
// require_once 'Zend/Pdf/Resource/Unified.php';
// require_once 'Zend/Pdf/Canvas/Abstract.php';
/**
* PDF Page
*
* @package Zend_Pdf
* @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Pdf_Page extends Zend_Pdf_Canvas_Abstract
{
/**** Class Constants ****/
/* Page Sizes */
/**
* Size representing an A4 page in portrait (tall) orientation.
*/
const SIZE_A4 = '595:842:';
/**
* Size representing an A4 page in landscape (wide) orientation.
*/
const SIZE_A4_LANDSCAPE = '842:595:';
/**
* Size representing a US Letter page in portrait (tall) orientation.
*/
const SIZE_LETTER = '612:792:';
/**
* Size representing a US Letter page in landscape (wide) orientation.
*/
const SIZE_LETTER_LANDSCAPE = '792:612:';
/* Shape Drawing */
/**
* Stroke the path only. Do not fill.
*/
const SHAPE_DRAW_STROKE = 0;
/**
* Fill the path only. Do not stroke.
*/
const SHAPE_DRAW_FILL = 1;
/**
* Fill and stroke the path.
*/
const SHAPE_DRAW_FILL_AND_STROKE = 2;
/* Shape Filling Methods */
/**
* Fill the path using the non-zero winding rule.
*/
const FILL_METHOD_NON_ZERO_WINDING = 0;
/**
* Fill the path using the even-odd rule.
*/
const FILL_METHOD_EVEN_ODD = 1;
/* Line Dash Types */
/**
* Solid line dash.
*/
const LINE_DASHING_SOLID = 0;
/**
* Page dictionary (refers to an inderect Zend_Pdf_Element_Dictionary object).
*
* @var Zend_Pdf_Element_Reference|Zend_Pdf_Element_Object
*/
protected $_dictionary;
/**
* PDF objects factory.
*
* @var Zend_Pdf_ElementFactory_Interface
*/
protected $_objFactory = null;
/**
* Flag which signals, that page is created separately from any PDF document or
* attached to anyone.
*
* @var boolean
*/
protected $_attached;
/**
* Safe Graphics State semafore
*
* If it's false, than we can't be sure Graphics State is restored withing
* context of previous contents stream (ex. drawing coordinate system may be rotated).
* We should encompass existing content with save/restore GS operators
*
* @var boolean
*/
protected $_safeGS;
/**
* Object constructor.
* Constructor signatures:
*
* 1. Load PDF page from a parsed PDF file.
* Object factory is created by PDF parser.
* ---------------------------------------------------------
* new Zend_Pdf_Page(Zend_Pdf_Element_Dictionary $pageDict,
* Zend_Pdf_ElementFactory_Interface $factory);
* ---------------------------------------------------------
*
* 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.
* ---------------------------------------------------------
* new Zend_Pdf_Page(Zend_Pdf_Page $page);
* ---------------------------------------------------------
*
* 3. Create new page with a specified pagesize.
* If $factory is null then it will be created and page must be attached to the document to be
* included into output.
* ---------------------------------------------------------
* new Zend_Pdf_Page(string $pagesize, Zend_Pdf_ElementFactory_Interface $factory = null);
* ---------------------------------------------------------
*
* 4. Create new page with a specified pagesize (in default user space units).
* If $factory is null then it will be created and page must be attached to the document to be
* included into output.
* ---------------------------------------------------------
* new Zend_Pdf_Page(numeric $width, numeric $height, Zend_Pdf_ElementFactory_Interface $factory = null);
* ---------------------------------------------------------
*
*
* @param mixed $param1
* @param mixed $param2
* @param mixed $param3
* @throws Zend_Pdf_Exception
*/
public function __construct($param1, $param2 = null, $param3 = null)
{
if (($param1 instanceof Zend_Pdf_Element_Reference ||
$param1 instanceof Zend_Pdf_Element_Object
) &&
$param2 instanceof Zend_Pdf_ElementFactory_Interface &&
$param3 === null
) {
switch ($param1->getType()) {
case Zend_Pdf_Element::TYPE_DICTIONARY:
$this->_dictionary = $param1;
$this->_objFactory = $param2;
$this->_attached = true;
$this->_safeGS = false;
return;
break;
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) {
// Duplicate existing page.
// Let already existing content and resources to be shared between pages
// We don't give existing content modification functionality, so we don't need "deep copy"
$this->_objFactory = $param1->_objFactory;
$this->_attached = &$param1->_attached;
$this->_safeGS = false;
$this->_dictionary = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
foreach ($param1->_dictionary->getKeys() as $key) {
if ($key == 'Contents') {
// Clone Contents property
$this->_dictionary->Contents = new Zend_Pdf_Element_Array();
if ($param1->_dictionary->Contents->getType() != Zend_Pdf_Element::TYPE_ARRAY) {
// Prepare array of content streams and add existing stream
$this->_dictionary->Contents->items[] = $param1->_dictionary->Contents;
} else {
// Clone array of the content streams
foreach ($param1->_dictionary->Contents->items as $srcContentStream) {
$this->_dictionary->Contents->items[] = $srcContentStream;
}
}
} else {
$this->_dictionary->$key = $param1->_dictionary->$key;
}
}
return;
} else if (is_string($param1) &&
($param2 === null || $param2 instanceof Zend_Pdf_ElementFactory_Interface) &&
$param3 === null) {
if ($param2 !== null) {
$this->_objFactory = $param2;
} else {
// require_once 'Zend/Pdf/ElementFactory.php';
$this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1);
}
$this->_attached = false;
$this->_safeGS = true; /** New page created. That's users App responsibility to track GS changes */
switch (strtolower($param1)) {
case 'a4':
$param1 = Zend_Pdf_Page::SIZE_A4;
break;
case 'a4-landscape':
$param1 = Zend_Pdf_Page::SIZE_A4_LANDSCAPE;
break;
case 'letter':
$param1 = Zend_Pdf_Page::SIZE_LETTER;
break;
case 'letter-landscape':
$param1 = Zend_Pdf_Page::SIZE_LETTER_LANDSCAPE;
break;
default:
// should be in "x:y" or "x:y:" form
}
$pageDim = explode(':', $param1);
if(count($pageDim) == 2 || count($pageDim) == 3) {
$pageWidth = $pageDim[0];
$pageHeight = $pageDim[1];
} else {
/**
* @todo support of user defined pagesize notations, like:
* "210x297mm", "595x842", "8.5x11in", "612x792"
*/
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Wrong pagesize notation.');
}
/**
* @todo support of pagesize recalculation to "default user space units"
*/
} else if (is_numeric($param1) && is_numeric($param2) &&
($param3 === null || $param3 instanceof Zend_Pdf_ElementFactory_Interface)) {
if ($param3 !== null) {
$this->_objFactory = $param3;
} else {
// require_once 'Zend/Pdf/ElementFactory.php';
$this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1);
}
$this->_attached = false;
$this->_safeGS = true; /** New page created. That's users App responsibility to track GS changes */
$pageWidth = $param1;
$pageHeight = $param2;
} else {
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Unrecognized method signature, wrong number of arguments or wrong argument types.');
}
$this->_dictionary = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
$this->_dictionary->Type = new Zend_Pdf_Element_Name('Page');
// require_once 'Zend/Pdf.php';
$this->_dictionary->LastModified = new Zend_Pdf_Element_String(Zend_Pdf::pdfDate());
$this->_dictionary->Resources = new Zend_Pdf_Element_Dictionary();
$this->_dictionary->MediaBox = new Zend_Pdf_Element_Array();
$this->_dictionary->MediaBox->items[] = new Zend_Pdf_Element_Numeric(0);
$this->_dictionary->MediaBox->items[] = new Zend_Pdf_Element_Numeric(0);
$this->_dictionary->MediaBox->items[] = new Zend_Pdf_Element_Numeric($pageWidth);
$this->_dictionary->MediaBox->items[] = new Zend_Pdf_Element_Numeric($pageHeight);
$this->_dictionary->Contents = new Zend_Pdf_Element_Array();
}
/**
* Attach resource to the canvas
*
* Method returns a name of the resource which can be used
* as a resource reference within drawing instructions stream
* Allowed types: 'ExtGState', 'ColorSpace', 'Pattern', 'Shading',
* 'XObject', 'Font', 'Properties'
*
* @param string $type
* @param Zend_Pdf_Resource $resource
* @return string
*/
protected function _attachResource($type, Zend_Pdf_Resource $resource)
{
// Check that Resources dictionary contains appropriate resource set
if ($this->_dictionary->Resources->$type === null) {
$this->_dictionary->Resources->touch();
$this->_dictionary->Resources->$type = new Zend_Pdf_Element_Dictionary();
} else {
$this->_dictionary->Resources->$type->touch();
}
// Check, that resource is already attached to resource set.
$resObject = $resource->getResource();
foreach ($this->_dictionary->Resources->$type->getKeys() as $ResID) {
if ($this->_dictionary->Resources->$type->$ResID === $resObject) {
return $ResID;
}
}
$idCounter = 1;
do {
$newResName = $type[0] . $idCounter++;
} while ($this->_dictionary->Resources->$type->$newResName !== null);
$this->_dictionary->Resources->$type->$newResName = $resObject;
$this->_objFactory->attach($resource->getFactory());
return $newResName;
}
/**
* Add procedureSet to the Page description
*
* @param string $procSetName
*/
protected function _addProcSet($procSetName)
{
// Check that Resources dictionary contains ProcSet entry
if ($this->_dictionary->Resources->ProcSet === null) {
$this->_dictionary->Resources->touch();
$this->_dictionary->Resources->ProcSet = new Zend_Pdf_Element_Array();
} else {
$this->_dictionary->Resources->ProcSet->touch();
}
foreach ($this->_dictionary->Resources->ProcSet->items as $procSetEntry) {
if ($procSetEntry->value == $procSetName) {
// Procset is already included into a ProcSet array
return;
}
}
$this->_dictionary->Resources->ProcSet->items[] = new Zend_Pdf_Element_Name($procSetName);
}
/**
* Returns dictionaries of used resources.
*
* Used for canvas implementations interoperability
*
* Structure of the returned array:
* array(
* <resTypeName> => array(
* <resName> => <Zend_Pdf_Resource object>,
* <resName> => <Zend_Pdf_Resource object>,
* <resName> => <Zend_Pdf_Resource object>,
* ...
* ),
* <resTypeName> => array(
* <resName> => <Zend_Pdf_Resource object>,
* <resName> => <Zend_Pdf_Resource object>,
* <resName> => <Zend_Pdf_Resource object>,
* ...
* ),
* ...
* 'ProcSet' => array()
* )
*
* where ProcSet array is a list of used procedure sets names (strings).
* Allowed procedure set names: 'PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'
*
* @internal
* @return array
*/
public function getResources()
{
$resources = array();
$resDictionary = $this->_dictionary->Resources;
foreach ($resDictionary->getKeys() as $resType) {
$resources[$resType] = array();
if ($resType == 'ProcSet') {
foreach ($resDictionary->ProcSet->items as $procSetEntry) {
$resources[$resType][] = $procSetEntry->value;
}
} else {
$resMap = $resDictionary->$resType;
foreach ($resMap->getKeys() as $resId) {
$resources[$resType][$resId] =new Zend_Pdf_Resource_Unified($resMap->$resId);
}
}
}
return $resources;
}
/**
* Get drawing instructions stream
*
* It has to be returned as a PDF stream object to make it reusable.
*
* @internal
* @returns Zend_Pdf_Resource_ContentStream
*/
public function getContents()
{
/** @todo implementation */
}
/**
* Return the height of this page in points.
*
* @return float
*/
public function getHeight()
{
return $this->_dictionary->MediaBox->items[3]->value -
$this->_dictionary->MediaBox->items[1]->value;
}
/**
* Return the width of this page in points.
*
* @return float
*/
public function getWidth()
{
return $this->_dictionary->MediaBox->items[2]->value -
$this->_dictionary->MediaBox->items[0]->value;
}
/**
* 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->_dictionary->getKeys() as $key) {
$dictionary->$key = $this->_dictionary->$key->makeClone($factory->getFactory(),
$processed,
Zend_Pdf_Element::CLONE_MODE_SKIP_PAGES);
}
$this->_dictionary = $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->_dictionary->getKeys() as $key) {
$dictionary->$key = $this->_dictionary->$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
* @return Zend_Pdf_Element_Dictionary
*/
public function getPageDictionary()
{
return $this->_dictionary;
}
/**
* Dump current drawing instructions into the content stream.
*
* @todo Don't forget to close all current graphics operations (like path drawing)
*
* @throws Zend_Pdf_Exception
*/
public function flush()
{
if ($this->_saveCount != 0) {
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Saved graphics state is not restored');
}
if ($this->_contents == '') {
return;
}
if ($this->_dictionary->Contents->getType() != Zend_Pdf_Element::TYPE_ARRAY) {
/**
* It's a stream object.
* Prepare Contents page attribute for update.
*/
$this->_dictionary->touch();
$currentPageContents = $this->_dictionary->Contents;
$this->_dictionary->Contents = new Zend_Pdf_Element_Array();
$this->_dictionary->Contents->items[] = $currentPageContents;
} else {
$this->_dictionary->Contents->touch();
}
if ((!$this->_safeGS) && (count($this->_dictionary->Contents->items) != 0)) {
/**
* Page already has some content which is not treated as safe.
*
* Add save/restore GS operators
*/
$this->_addProcSet('PDF');
$newContentsArray = new Zend_Pdf_Element_Array();
$newContentsArray->items[] = $this->_objFactory->newStreamObject(" q\n");
foreach ($this->_dictionary->Contents->items as $contentStream) {
$newContentsArray->items[] = $contentStream;
}
$newContentsArray->items[] = $this->_objFactory->newStreamObject(" Q\n");
$this->_dictionary->touch();
$this->_dictionary->Contents = $newContentsArray;
$this->_safeGS = true;
}
$this->_dictionary->Contents->items[] =
$this->_objFactory->newStreamObject($this->_contents);
$this->_contents = '';
}
/**
* Prepare page to be rendered into PDF.
*
* @todo Don't forget to close all current graphics operations (like path drawing)
*
* @param Zend_Pdf_ElementFactory_Interface $objFactory
* @throws Zend_Pdf_Exception
*/
public function render(Zend_Pdf_ElementFactory_Interface $objFactory)
{
$this->flush();
if ($objFactory === $this->_objFactory) {
// Page is already attached to the document.
return;
}
if ($this->_attached) {
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Page is attached to other documen. Use clone $page to get it context free.');
} else {
$objFactory->attach($this->_objFactory);
}
}
/**
* Extract resources attached to the page
*
* This method is not intended to be used in userland, but helps to optimize some document wide operations
*
* returns array of Zend_Pdf_Element_Dictionary objects
*
* @internal
* @return array
*/
public function extractResources()
{
return $this->_dictionary->Resources;
}
/**
* Extract fonts attached to the page
*
* returns array of Zend_Pdf_Resource_Font_Extracted objects
*
* @return array
* @throws Zend_Pdf_Exception
*/
public function extractFonts()
{
if ($this->_dictionary->Resources->Font === null) {
// Page doesn't have any font attached
// Return empty array
return array();
}
$fontResources = $this->_dictionary->Resources->Font;
$fontResourcesUnique = array();
foreach ($fontResources->getKeys() as $fontResourceName) {
$fontDictionary = $fontResources->$fontResourceName;
if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference ||
$fontDictionary instanceof Zend_Pdf_Element_Object) ) {
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.');
}
$fontResourcesUnique[spl_object_hash($fontDictionary->getObject())] = $fontDictionary;
}
$fonts = array();
// require_once 'Zend/Pdf/Exception.php';
foreach ($fontResourcesUnique as $resourceId => $fontDictionary) {
try {
// require_once 'Zend/Pdf/Resource/Font/Extracted.php';
// Try to extract font
$extractedFont = new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
$fonts[$resourceId] = $extractedFont;
} catch (Zend_Pdf_Exception $e) {
if ($e->getMessage() != 'Unsupported font type.') {
throw new Zend_Pdf_Exception($e->getMessage(), $e->getCode(), $e);
}
}
}
return $fonts;
}
/**
* Extract font attached to the page by specific font name
*
* $fontName should be specified in UTF-8 encoding
*
* @return Zend_Pdf_Resource_Font_Extracted|null
* @throws Zend_Pdf_Exception
*/
public function extractFont($fontName)
{
if ($this->_dictionary->Resources->Font === null) {
// Page doesn't have any font attached
return null;
}
$fontResources = $this->_dictionary->Resources->Font;
$fontResourcesUnique = array();
// require_once 'Zend/Pdf/Exception.php';
foreach ($fontResources->getKeys() as $fontResourceName) {
$fontDictionary = $fontResources->$fontResourceName;
if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference ||
$fontDictionary instanceof Zend_Pdf_Element_Object) ) {
// require_once 'Zend/Pdf/Exception.php';
throw new Zend_Pdf_Exception('Font dictionary has to be an indirect object or object reference.');
}
$resourceId = spl_object_hash($fontDictionary->getObject());
if (isset($fontResourcesUnique[$resourceId])) {
continue;
} else {
// Mark resource as processed
$fontResourcesUnique[$resourceId] = 1;
}
if ($fontDictionary->BaseFont->value != $fontName) {
continue;
}
try {
// Try to extract font
// require_once 'Zend/Pdf/Resource/Font/Extracted.php';
return new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
} catch (Zend_Pdf_Exception $e) {
if ($e->getMessage() != 'Unsupported font type.') {
throw new Zend_Pdf_Exception($e->getMessage(), $e->getCode(), $e);
}
// Continue searhing font with specified name
}
}
return null;
}
/**
*
* @param Zend_Pdf_Annotation $annotation
* @return Zend_Pdf_Page
*/
public function attachAnnotation(Zend_Pdf_Annotation $annotation)
{
$annotationDictionary = $annotation->getResource();
if (!$annotationDictionary instanceof Zend_Pdf_Element_Object &&
!$annotationDictionary instanceof Zend_Pdf_Element_Reference) {
$annotationDictionary = $this->_objFactory->newObject($annotationDictionary);
}
if ($this->_dictionary->Annots === null) {
$this->_dictionary->touch();
$this->_dictionary->Annots = new Zend_Pdf_Element_Array();
} else {
$this->_dictionary->Annots->touch();
}
$this->_dictionary->Annots->items[] = $annotationDictionary;
$annotationDictionary->touch();
$annotationDictionary->P = $this->_dictionary;
return $this;
}
}