Pdf.php 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Pdf
  17. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  18. * @license http://framework.zend.com/license/new-bsd New BSD License
  19. * @version $Id$
  20. */
  21. /** Zend_Pdf_Page */
  22. require_once 'Zend/Pdf/Page.php';
  23. /** Zend_Pdf_Cmap */
  24. require_once 'Zend/Pdf/Cmap.php';
  25. /** Zend_Pdf_Font */
  26. require_once 'Zend/Pdf/Font.php';
  27. /** Zend_Pdf_Style */
  28. require_once 'Zend/Pdf/Style.php';
  29. /** Zend_Pdf_Parser */
  30. require_once 'Zend/Pdf/Parser.php';
  31. /** Zend_Pdf_Trailer */
  32. require_once 'Zend/Pdf/Trailer.php';
  33. /** Zend_Pdf_Trailer_Generator */
  34. require_once 'Zend/Pdf/Trailer/Generator.php';
  35. /** Zend_Pdf_Color */
  36. require_once 'Zend/Pdf/Color.php';
  37. /** Zend_Pdf_Color_GrayScale */
  38. require_once 'Zend/Pdf/Color/GrayScale.php';
  39. /** Zend_Pdf_Color_Rgb */
  40. require_once 'Zend/Pdf/Color/Rgb.php';
  41. /** Zend_Pdf_Color_Cmyk */
  42. require_once 'Zend/Pdf/Color/Cmyk.php';
  43. /** Zend_Pdf_Color_Html */
  44. require_once 'Zend/Pdf/Color/Html.php';
  45. /** Zend_Pdf_Image */
  46. require_once 'Zend/Pdf/Resource/Image.php';
  47. /** Zend_Pdf_Image */
  48. require_once 'Zend/Pdf/Image.php';
  49. /** Zend_Pdf_Image_Jpeg */
  50. require_once 'Zend/Pdf/Resource/Image/Jpeg.php';
  51. /** Zend_Pdf_Image_Tiff */
  52. require_once 'Zend/Pdf/Resource/Image/Tiff.php';
  53. /** Zend_Pdf_Image_Png */
  54. require_once 'Zend/Pdf/Resource/Image/Png.php';
  55. /** Zend_Memory */
  56. require_once 'Zend/Memory.php';
  57. /** Zend_Pdf_Action */
  58. require_once 'Zend/Pdf/Action.php';
  59. /** Zend_Pdf_Destination */
  60. require_once 'Zend/Pdf/Destination.php';
  61. /** Zend_Pdf_Destination */
  62. require_once 'Zend/Pdf/Exception.php';
  63. /**
  64. * General entity which describes PDF document.
  65. * It implements document abstraction with a document level operations.
  66. *
  67. * Class is used to create new PDF document or load existing document.
  68. * See details in a class constructor description
  69. *
  70. * Class agregates document level properties and entities (pages, bookmarks,
  71. * document level actions, attachments, form object, etc)
  72. *
  73. * @category Zend
  74. * @package Zend_Pdf
  75. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  76. * @license http://framework.zend.com/license/new-bsd New BSD License
  77. */
  78. class Zend_Pdf
  79. {
  80. /**** Class Constants ****/
  81. /**
  82. * Version number of generated PDF documents.
  83. */
  84. const PDF_VERSION = '1.4';
  85. /**
  86. * PDF file header.
  87. */
  88. const PDF_HEADER = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
  89. /**
  90. * Pages collection
  91. *
  92. * @todo implement it as a class, which supports ArrayAccess and Iterator interfaces,
  93. * to provide incremental parsing and pages tree updating.
  94. * That will give good performance and memory (PDF size) benefits.
  95. *
  96. * @var array - array of Zend_Pdf_Page object
  97. */
  98. public $pages = array();
  99. /**
  100. * Document properties
  101. *
  102. * It's an associative array with PDF meta information, values may
  103. * be string, boolean or float.
  104. * Returned array could be used directly to access, add, modify or remove
  105. * document properties.
  106. *
  107. * Standard document properties: Title (must be set for PDF/X documents), Author,
  108. * Subject, Keywords (comma separated list), Creator (the name of the application,
  109. * that created document, if it was converted from other format), Trapped (must be
  110. * true, false or null, can not be null for PDF/X documents)
  111. *
  112. * @var array
  113. */
  114. public $properties = array();
  115. /**
  116. * Original properties set.
  117. *
  118. * Used for tracking properties changes
  119. *
  120. * @var array
  121. */
  122. protected $_originalProperties = array();
  123. /**
  124. * Document level javascript
  125. *
  126. * @var string
  127. */
  128. protected $_javaScript = null;
  129. /**
  130. * Document named actions
  131. * "GoTo..." actions, used to refer document parts
  132. * from outside PDF
  133. *
  134. * @var array - array of Zend_Pdf_Action objects
  135. */
  136. protected $_namedActions = array();
  137. /**
  138. * Document named destinations
  139. *
  140. * @var array - array of Zend_Pdf_Destinations objects
  141. */
  142. protected $_namedDestinations = array();
  143. /**
  144. * Pdf trailer (last or just created)
  145. *
  146. * @var Zend_Pdf_Trailer
  147. */
  148. protected $_trailer = null;
  149. /**
  150. * PDF objects factory.
  151. *
  152. * @var Zend_Pdf_ElementFactory_Interface
  153. */
  154. protected $_objFactory = null;
  155. /**
  156. * Memory manager for stream objects
  157. *
  158. * @var Zend_Memory_Manager|null
  159. */
  160. protected static $_memoryManager = null;
  161. /**
  162. * Pdf file parser.
  163. * It's not used, but has to be destroyed only with Zend_Pdf object
  164. *
  165. * @var Zend_Pdf_Parser
  166. */
  167. protected $_parser;
  168. /**
  169. * List of inheritable attributesfor pages tree
  170. *
  171. * @var array
  172. */
  173. protected static $_inheritableAttributes = array('Resources', 'MediaBox', 'CropBox', 'Rotate');
  174. /**
  175. * Request used memory manager
  176. *
  177. * @return Zend_Memory_Manager
  178. */
  179. static public function getMemoryManager()
  180. {
  181. if (self::$_memoryManager === null) {
  182. self::$_memoryManager = Zend_Memory::factory('none');
  183. }
  184. return self::$_memoryManager;
  185. }
  186. /**
  187. * Set user defined memory manager
  188. *
  189. * @param Zend_Memory_Manager $memoryManager
  190. */
  191. static public function setMemoryManager(Zend_Memory_Manager $memoryManager)
  192. {
  193. self::$_memoryManager = $memoryManager;
  194. }
  195. /**
  196. * Create new PDF document from a $source string
  197. *
  198. * @param string $source
  199. * @param integer $revision
  200. * @return Zend_Pdf
  201. */
  202. public static function parse(&$source = null, $revision = null)
  203. {
  204. return new Zend_Pdf($source, $revision);
  205. }
  206. /**
  207. * Load PDF document from a file
  208. *
  209. * @param string $source
  210. * @param integer $revision
  211. * @return Zend_Pdf
  212. */
  213. public static function load($source = null, $revision = null)
  214. {
  215. return new Zend_Pdf($source, $revision, true);
  216. }
  217. /**
  218. * Render PDF document and save it.
  219. *
  220. * If $updateOnly is true, then it only appends new section to the end of file.
  221. *
  222. * @param string $filename
  223. * @param boolean $updateOnly
  224. * @throws Zend_Pdf_Exception
  225. */
  226. public function save($filename, $updateOnly = false)
  227. {
  228. if (($file = @fopen($filename, $updateOnly ? 'ab':'wb')) === false ) {
  229. require_once 'Zend/Pdf/Exception.php';
  230. throw new Zend_Pdf_Exception( "Can not open '$filename' file for writing." );
  231. }
  232. $this->render($updateOnly, $file);
  233. fclose($file);
  234. }
  235. /**
  236. * Parse destination structure (array or dictionary) and
  237. * return it as a Zend_Pdf_Destination or Zend_Pdf_Action object
  238. *
  239. * $param Zend_Pdf_Element $destination
  240. * @return Zend_Pdf_Destination|
  241. * @throws Zend_Pdf_Exception
  242. */
  243. protected function _loadDestination(Zend_Pdf_Element $destination) {
  244. if ($destination->getType() == Zend_Pdf_Element::TYPE_ARRAY) {
  245. // Destination is an array, just treat it as an explicit destination array
  246. return Zend_Pdf_Destination::load($destination);
  247. } else if ($destination->getType() == Zend_Pdf_Element::TYPE_DICTIONARY) {
  248. // Load destination as appropriate action
  249. return Zend_Pdf_Action::load($destination);
  250. } else {
  251. require_once 'Zend/Pdf/Exception.php';
  252. throw new Zend_Pdf_Exception( 'PDF named destination entry must be an array or dictionary.' );
  253. }
  254. }
  255. /**
  256. * Creates or loads PDF document.
  257. *
  258. * If $source is null, then it creates a new document.
  259. *
  260. * If $source is a string and $load is false, then it loads document
  261. * from a binary string.
  262. *
  263. * If $source is a string and $load is true, then it loads document
  264. * from a file.
  265. * $revision used to roll back document to specified version
  266. * (0 - currtent version, 1 - previous version, 2 - ...)
  267. *
  268. * @param string $source - PDF file to load
  269. * @param integer $revision
  270. * @throws Zend_Pdf_Exception
  271. * @return Zend_Pdf
  272. */
  273. public function __construct($source = null, $revision = null, $load = false)
  274. {
  275. $this->_objFactory = Zend_Pdf_ElementFactory::createFactory(1);
  276. if ($source !== null) {
  277. $this->_parser = new Zend_Pdf_Parser($source, $this->_objFactory, $load);
  278. $this->_pdfHeaderVersion = $this->_parser->getPDFVersion();
  279. $this->_trailer = $this->_parser->getTrailer();
  280. if ($this->_trailer->Encrypt !== null) {
  281. require_once 'Zend/Pdf/Exception.php';
  282. throw new Zend_Pdf_Exception('Encrypted document modification is not supported');
  283. }
  284. if ($revision !== null) {
  285. $this->rollback($revision);
  286. } else {
  287. $this->_loadPages($this->_trailer->Root->Pages);
  288. }
  289. if ($this->_trailer->Info !== null) {
  290. $this->properties = $this->_trailer->Info->toPhp();
  291. if (isset($this->properties['Trapped'])) {
  292. switch ($this->properties['Trapped']) {
  293. case 'True':
  294. $this->properties['Trapped'] = true;
  295. break;
  296. case 'False':
  297. $this->properties['Trapped'] = false;
  298. break;
  299. case 'Unknown':
  300. $this->properties['Trapped'] = null;
  301. break;
  302. default:
  303. // Wrong property value
  304. // Do nothing
  305. break;
  306. }
  307. }
  308. $this->_originalProperties = $this->properties;
  309. }
  310. // Collect named destinations (exclude not referenced pages)
  311. $root = $this->_trailer->Root;
  312. $pdfHeaderVersion = $this->_parser->getPDFVersion();
  313. if ($root->Version !== null && version_compare($root->Version->value, $pdfHeaderVersion, '>')) {
  314. $versionIs_1_2_plus = version_compare($root->Version->value, '1.1', '>');
  315. } else {
  316. $versionIs_1_2_plus = version_compare($pdfHeaderVersion, '1.1', '>');
  317. }
  318. if ($versionIs_1_2_plus) {
  319. // PDF version is 1.2+
  320. // Look for Destinations structure at Name dictionary
  321. if ($root->Names !== null && $root->Names->Dests !== null) {
  322. $intermediateNodes = array();
  323. $leafNodes = array();
  324. if ($root->Names->Dests->Kids !== null) {
  325. $intermediateNodes[] = $root->Names->Dests;
  326. } else {
  327. $leafNodes[] = $root->Names->Dests;
  328. }
  329. while (count($intermediateNodes) != 0) {
  330. $newIntermediateNodes = array();
  331. foreach ($intermediateNodes as $node) {
  332. foreach ($node->Kids->items as $childNode) {
  333. if ($childNode->Kids !== null) {
  334. $newIntermediateNodes[] = $childNode;
  335. } else {
  336. $leafNodes[] = $childNode;
  337. }
  338. }
  339. }
  340. $intermediateNodes = $newIntermediateNodes;
  341. }
  342. foreach ($leafNodes as $leafNode) {
  343. $destinationsCount = count($leafNode->items)/2;
  344. for ($count = 0; $count < $destinationsCount; $count++) {
  345. $destinationName = $leafNode->items[$count*2];
  346. $destination = $this->_loadDestination($leafNode->items[$count*2 + 1]);
  347. if ($destination instanceof Zend_Pdf_Action) {
  348. $this->_namedActions[$destKey] = $destination;
  349. } else {
  350. $this->_namedDestinations[$destKey] = $destination;
  351. }
  352. }
  353. }
  354. }
  355. } else {
  356. // PDF version is 1.1 (or earlier)
  357. // Look for Destinations sructure at Dest entry of document catalog
  358. if ($root->Dests !== null) {
  359. if ($root->Dests->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
  360. require_once 'Zend/Pdf/Exception.php';
  361. throw new Zend_Pdf_Exception( 'Document catalog Dests entry must be a dictionary.' );
  362. }
  363. foreach ($root->Dests->getKeys() as $destKey) {
  364. $destination = $this->_loadDestination($root->Dests->$destKey);
  365. if ($destination instanceof Zend_Pdf_Action) {
  366. $this->_namedActions[$destKey] = $destination;
  367. } else {
  368. $this->_namedDestinations[$destKey] = $destination;
  369. }
  370. }
  371. }
  372. }
  373. } else {
  374. $this->_pdfHeaderVersion = Zend_Pdf::PDF_VERSION;
  375. $trailerDictionary = new Zend_Pdf_Element_Dictionary();
  376. /**
  377. * Document id
  378. */
  379. $docId = md5(uniqid(rand(), true)); // 32 byte (128 bit) identifier
  380. $docIdLow = substr($docId, 0, 16); // first 16 bytes
  381. $docIdHigh = substr($docId, 16, 16); // second 16 bytes
  382. $trailerDictionary->ID = new Zend_Pdf_Element_Array();
  383. $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdLow);
  384. $trailerDictionary->ID->items[] = new Zend_Pdf_Element_String_Binary($docIdHigh);
  385. $trailerDictionary->Size = new Zend_Pdf_Element_Numeric(0);
  386. $this->_trailer = new Zend_Pdf_Trailer_Generator($trailerDictionary);
  387. /**
  388. * Document catalog indirect object.
  389. */
  390. $docCatalog = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
  391. $docCatalog->Type = new Zend_Pdf_Element_Name('Catalog');
  392. $docCatalog->Version = new Zend_Pdf_Element_Name(Zend_Pdf::PDF_VERSION);
  393. $this->_trailer->Root = $docCatalog;
  394. /**
  395. * Pages container
  396. */
  397. $docPages = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
  398. $docPages->Type = new Zend_Pdf_Element_Name('Pages');
  399. $docPages->Kids = new Zend_Pdf_Element_Array();
  400. $docPages->Count = new Zend_Pdf_Element_Numeric(0);
  401. $docCatalog->Pages = $docPages;
  402. }
  403. }
  404. /**
  405. * Destructor
  406. * Clean up resources
  407. */
  408. public function __destruct()
  409. {
  410. foreach ($this->_namedActions as $action) {
  411. $action->clean();
  412. }
  413. }
  414. /**
  415. * Retrive number of revisions.
  416. *
  417. * @return integer
  418. */
  419. public function revisions()
  420. {
  421. $revisions = 1;
  422. $currentTrailer = $this->_trailer;
  423. while ($currentTrailer->getPrev() !== null && $currentTrailer->getPrev()->Root !== null ) {
  424. $revisions++;
  425. $currentTrailer = $currentTrailer->getPrev();
  426. }
  427. return $revisions++;
  428. }
  429. /**
  430. * Rollback document $steps number of revisions.
  431. * This method must be invoked before any changes, applied to the document.
  432. * Otherwise behavior is undefined.
  433. *
  434. * @param integer $steps
  435. */
  436. public function rollback($steps)
  437. {
  438. for ($count = 0; $count < $steps; $count++) {
  439. if ($this->_trailer->getPrev() !== null && $this->_trailer->getPrev()->Root !== null) {
  440. $this->_trailer = $this->_trailer->getPrev();
  441. } else {
  442. break;
  443. }
  444. }
  445. $this->_objFactory->setObjectCount($this->_trailer->Size->value);
  446. // Mark content as modified to force new trailer generation at render time
  447. $this->_trailer->Root->touch();
  448. $this->pages = array();
  449. $this->_loadPages($this->_trailer->Root->Pages);
  450. }
  451. /**
  452. * Load pages recursively
  453. *
  454. * @param Zend_Pdf_Element_Reference $pages
  455. * @param array|null $attributes
  456. */
  457. protected function _loadPages(Zend_Pdf_Element_Reference $pages, $attributes = array())
  458. {
  459. if ($pages->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
  460. require_once 'Zend/Pdf/Exception.php';
  461. throw new Zend_Pdf_Exception('Wrong argument');
  462. }
  463. foreach ($pages->getKeys() as $property) {
  464. if (in_array($property, self::$_inheritableAttributes)) {
  465. $attributes[$property] = $pages->$property;
  466. $pages->$property = null;
  467. }
  468. }
  469. foreach ($pages->Kids->items as $child) {
  470. if ($child->Type->value == 'Pages') {
  471. $this->_loadPages($child, $attributes);
  472. } else if ($child->Type->value == 'Page') {
  473. foreach (self::$_inheritableAttributes as $property) {
  474. if ($child->$property === null && array_key_exists($property, $attributes)) {
  475. /**
  476. * Important note.
  477. * If any attribute or dependant object is an indirect object, then it's still
  478. * shared between pages.
  479. */
  480. if ($attributes[$property] instanceof Zend_Pdf_Element_Object) {
  481. $child->$property = $attributes[$property];
  482. } else {
  483. $child->$property = $this->_objFactory->newObject($attributes[$property]);
  484. }
  485. }
  486. }
  487. $this->pages[] = new Zend_Pdf_Page($child, $this->_objFactory);
  488. }
  489. }
  490. }
  491. /**
  492. * Orginize pages to tha pages tree structure.
  493. *
  494. * @todo atomatically attach page to the document, if it's not done yet.
  495. * @todo check, that page is attached to the current document
  496. *
  497. * @todo Dump pages as a balanced tree instead of a plain set.
  498. */
  499. protected function _dumpPages()
  500. {
  501. $root = $this->_trailer->Root;
  502. $pagesContainer = $root->Pages;
  503. $pagesContainer->touch();
  504. $pagesContainer->Kids->items->clear();
  505. $pageDictionaries = new SplObjectStorage();
  506. foreach ($this->pages as $page ) {
  507. $page->render($this->_objFactory);
  508. $pageDictionary = $page->getPageDictionary();
  509. $pageDictionary->touch();
  510. $pageDictionary->Parent = $pagesContainer;
  511. $pagesContainer->Kids->items[] = $pageDictionary;
  512. // Collect page dictionary
  513. $pageDictionaries->attach($pageDictionary);
  514. }
  515. $pagesContainer->Count->touch();
  516. $pagesContainer->Count->value = count($this->pages);
  517. // Refresh named actions list
  518. foreach ($this->_namedActions as $name => $namedAction) {
  519. $rootAction = $namedAction;
  520. // Walk through chained actions
  521. foreach ($namedAction->getAllActions() as $chainedAction) {
  522. if ($chainedAction instanceof Zend_Pdf_Action_GoTo) {
  523. $destination = $chainedAction->getDestination();
  524. if (!$destination instanceof Zend_Pdf_Destination) {
  525. require_once 'Zend/Pdf/Exception.php';
  526. throw new Zend_Pdf_Exception('PDF named actions (destinations) must refer target as an explicit destination.');
  527. }
  528. $target = $destination->getTarget();
  529. if ($target instanceof Zend_Pdf_Element) {
  530. if (!$pageDictionaries->contains($target)) {
  531. $rootAction = $chainedAction->extract();
  532. }
  533. } else if ($target > count($this->pages) ) {
  534. $rootAction = $chainedAction->extract();
  535. }
  536. }
  537. }
  538. if ($rootAction === null) {
  539. unset($this->_namedActions[$name]);
  540. } else {
  541. $rootAction->rebuildSubtree();
  542. $this->_namedActions[$name] = $rootAction;
  543. }
  544. }
  545. // Refresh named destinations list
  546. foreach ($this->_namedDestinations as $name => $destination) {
  547. $target = $destination->getTarget();
  548. if ($target instanceof Zend_Pdf_Element) {
  549. if (!$pageDictionaries->contains($target)) {
  550. unset($this->_namedDestinations[$name]);
  551. }
  552. } else if ($target > count($this->pages) ) {
  553. unset($this->_namedDestinations[$name]);
  554. }
  555. }
  556. $openAction = $this->getOpenAction();
  557. if ($openAction !== null) {
  558. if ($openAction instanceof Zend_Pdf_Action) {
  559. $rootAction = $openAction;
  560. // Walk through chained actions
  561. foreach ($openAction->getAllActions() as $chainedAction) {
  562. if ($chainedAction instanceof Zend_Pdf_Action_GoTo) {
  563. $destination = $chainedAction->getDestination();
  564. if (!$destination instanceof Zend_Pdf_Destination) {
  565. // Look for $destination within named destinations
  566. if (!isset($this->_namedActions[$destination]) && !isset($this->_namedDestinations[$destination])) {
  567. $rootAction = $chainedAction->extract();
  568. }
  569. } else {
  570. // Destination is Zend_Pdf_Destination object
  571. $target = $destination->getTarget();
  572. if ($target instanceof Zend_Pdf_Element) {
  573. // which refers some page dictionary object
  574. // (check if it's within a collected dictionaries for current document)
  575. if (!$pageDictionaries->contains($target)) {
  576. $rootAction = $chainedAction->extract();
  577. }
  578. } else if ($target > count($this->pages) ) {
  579. // it's a page number, check if we have enough pages
  580. $rootAction = $chainedAction->extract();
  581. }
  582. }
  583. }
  584. }
  585. if ($rootAction !== null) {
  586. $rootAction->rebuildSubtree();
  587. $this->setOpenAction($rootAction);
  588. $rootAction->clean();
  589. } else {
  590. $this->setOpenAction(null);
  591. }
  592. } else if ($openAction instanceof Zend_Pdf_Destination) {
  593. $target = $openAction->getTarget();
  594. if ($target instanceof Zend_Pdf_Element) {
  595. if (!$pageDictionaries->contains($target)) {
  596. $this->setOpenAction(null);
  597. }
  598. } else if ($target > count($this->pages) ) {
  599. $this->setOpenAction(null);
  600. }
  601. } else {
  602. require_once 'Zend/Pdf/Exception.php';
  603. throw new Zend_Pdf_Exception('OpenAction has to be either PDF Action or Destination.');
  604. }
  605. }
  606. }
  607. /**
  608. * Dump named destinations
  609. *
  610. * @todo Create a balanced tree instead of plain structure.
  611. */
  612. protected function _dumpNamedDestinations()
  613. {
  614. $namedDestinations = $this->_namedActions + $this->_namedDestinations;
  615. ksort($namedDestinations, SORT_STRING);
  616. $destArray = $this->_objFactory->newObject(new Zend_Pdf_Element_Array());
  617. $destArrayItems = $destArray->items;
  618. foreach ($namedDestinations as $name => $destination) {
  619. $destArrayItems[] = new Zend_Pdf_Element_String($name);
  620. if ($destination instanceof Zend_Pdf_Action) {
  621. $destArrayItems[] = $destination->getDictionary();
  622. } else if ($destination instanceof Zend_Pdf_Destination) {
  623. $destArrayItems[] = $destination->getDestinationArray();
  624. } else {
  625. require_once 'Zend/Pdf/Exception.php';
  626. throw new Zend_Pdf_Exception('PDF named destinations must be Zend_Pdf_Action or Zend_Pdf_Destination objects.');
  627. }
  628. }
  629. $DestTree = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
  630. $DestTree->Names = $destArray;
  631. $root = $this->_trailer->Root;
  632. if ($root->Names === null) {
  633. $root->touch();
  634. $root->Names = new Zend_Pdf_Element_Dictionary();
  635. } else {
  636. $root->Names->touch();
  637. }
  638. $root->Names = $DestTree;
  639. }
  640. /**
  641. * Create page object, attached to the PDF document.
  642. * Method signatures:
  643. *
  644. * 1. Create new page with a specified pagesize.
  645. * If $factory is null then it will be created and page must be attached to the document to be
  646. * included into output.
  647. * ---------------------------------------------------------
  648. * new Zend_Pdf_Page(string $pagesize);
  649. * ---------------------------------------------------------
  650. *
  651. * 2. Create new page with a specified pagesize (in default user space units).
  652. * If $factory is null then it will be created and page must be attached to the document to be
  653. * included into output.
  654. * ---------------------------------------------------------
  655. * new Zend_Pdf_Page(numeric $width, numeric $height);
  656. * ---------------------------------------------------------
  657. *
  658. * @param mixed $param1
  659. * @param mixed $param2
  660. * @return Zend_Pdf_Page
  661. */
  662. public function newPage($param1, $param2 = null)
  663. {
  664. if ($param2 === null) {
  665. return new Zend_Pdf_Page($param1, $this->_objFactory);
  666. } else {
  667. return new Zend_Pdf_Page($param1, $param2, $this->_objFactory);
  668. }
  669. }
  670. /**
  671. * Return the document-level Metadata
  672. * or null Metadata stream is not presented
  673. *
  674. * @return string
  675. */
  676. public function getMetadata()
  677. {
  678. if ($this->_trailer->Root->Metadata !== null) {
  679. return $this->_trailer->Root->Metadata->value;
  680. } else {
  681. return null;
  682. }
  683. }
  684. /**
  685. * Sets the document-level Metadata (mast be valid XMP document)
  686. *
  687. * @param string $metadata
  688. */
  689. public function setMetadata($metadata)
  690. {
  691. $metadataObject = $this->_objFactory->newStreamObject($metadata);
  692. $metadataObject->dictionary->Type = new Zend_Pdf_Element_Name('Metadata');
  693. $metadataObject->dictionary->Subtype = new Zend_Pdf_Element_Name('XML');
  694. $this->_trailer->Root->Metadata = $metadataObject;
  695. $this->_trailer->Root->touch();
  696. }
  697. /**
  698. * Return the document-level JavaScript
  699. * or null if there is no JavaScript for this document
  700. *
  701. * @return string
  702. */
  703. public function getJavaScript()
  704. {
  705. return $this->_javaScript;
  706. }
  707. /**
  708. * Get open Action
  709. * Returns Zend_Pdf_Destination or Zend_Pdf_Action (which is actually GoTo Action) object
  710. *
  711. * @return Zend_Pdf_Destination|Zend_Pdf_Action
  712. */
  713. public function getOpenAction()
  714. {
  715. if ($this->_trailer->Root->OpenAction !== null) {
  716. return $this->_loadDestination($this->_trailer->Root->OpenAction);
  717. } else {
  718. return null;
  719. }
  720. }
  721. /**
  722. * Set open Action which is actually Zend_Pdf_Destination or Zend_Pdf_Action object
  723. *
  724. * @param Zend_Pdf_Destination|Zend_Pdf_Action $openAction
  725. * @throws Zend_Pdf_Exception
  726. */
  727. public function setOpenAction($openAction)
  728. {
  729. $root = $this->_trailer->Root;
  730. $root->touch();
  731. if ($openAction instanceof Zend_Pdf_Destination) {
  732. $root->OpenAction = $openAction->getDestinationArray();
  733. } if ($openAction instanceof Zend_Pdf_Action) {
  734. $root->OpenAction = $openAction->getDictionary();
  735. } if ($openAction === null) {
  736. $root->OpenAction = null;
  737. } else {
  738. require_once 'Zend/Pdf/Exception.php';
  739. throw new Zend_Pdf_Exception('Open action must be a Zend_Pdf_Destination or Zend_Pdf_Action objects or null.');
  740. }
  741. }
  742. /**
  743. * Return an associative array containing all the named actions in the PDF.
  744. * Named actions (it's always "GoTo" actions) can be used to reference from outside
  745. * the PDF, ex: 'http://www.something.com/mydocument.pdf#MyAction'
  746. *
  747. * @return array
  748. */
  749. public function getNamedActions()
  750. {
  751. return $this->_namedActions;
  752. }
  753. /**
  754. * Return specified named action
  755. *
  756. * @param string $name
  757. * @return Zend_Pdf_Action
  758. */
  759. public function getNamedAction($name)
  760. {
  761. if (isset($this->_namedActions[$name])) {
  762. return $this->_namedActions[$name];
  763. } else {
  764. return null;
  765. }
  766. }
  767. /**
  768. * Set specified named action
  769. *
  770. * @param string $name
  771. * @param Zend_Pdf_Action_GoTo $action
  772. */
  773. public function setNamedAction($name, Zend_Pdf_Action_GoTo $action)
  774. {
  775. if (isset($this->_namedActions[$name])) {
  776. $this->_namedActions[$name]->clean();
  777. }
  778. // Clean corresponding named destination if set
  779. unset($this->_namedDestinations[$name]);
  780. if (!$action->getDestination() instanceof Zend_Pdf_Destination) {
  781. require_once 'Zend/Pdf/Exception.php';
  782. throw new Zend_Pdf_Exception('PDF named actions (destinations) must refer target as an explicit destination.');
  783. }
  784. if ($action !== null) {
  785. $this->_namedActions[$name] = $action;
  786. } else {
  787. unset($this->_namedActions[$name]);
  788. }
  789. }
  790. /**
  791. * Return an associative array containing all the named destinationss in the PDF.
  792. *
  793. * @return array
  794. */
  795. public function getNamedDestinations()
  796. {
  797. return $this->_namedDestinations;
  798. }
  799. /**
  800. * Return specified named destination
  801. *
  802. * @param string $name
  803. * @return Zend_Pdf_Destination
  804. */
  805. public function getNamedDestination($name)
  806. {
  807. if (isset($this->_namedDestinations[$name])) {
  808. return $this->_namedDestinations[$name];
  809. } else {
  810. return null;
  811. }
  812. }
  813. /**
  814. * Set specified named action
  815. *
  816. * @param string $name
  817. * @param Zend_Pdf_Destination $destination
  818. */
  819. public function setNamedDestination($name, Zend_Pdf_Destination $destination)
  820. {
  821. // Clean corresponding named action if set
  822. if (isset($this->_namedActions[$name])) {
  823. $this->_namedActions[$name]->clean();
  824. unset($this->_namedActions[$name]);
  825. }
  826. if ($destination !== null) {
  827. $this->_namedDestinations[$name] = $destination;
  828. } else {
  829. unset($this->_namedDestinations[$name]);
  830. }
  831. }
  832. /**
  833. * Extract fonts attached to the document
  834. *
  835. * returns array of Zend_Pdf_Resource_Font_Extracted objects
  836. *
  837. * @return array
  838. */
  839. public function extractFonts()
  840. {
  841. $fontResourcesUnique = array();
  842. foreach ($this->pages as $page) {
  843. $pageResources = $page->extractResources();
  844. if ($pageResources->Font === null) {
  845. // Page doesn't contain have any font reference
  846. continue;
  847. }
  848. $fontResources = $pageResources->Font;
  849. foreach ($fontResources->getKeys() as $fontResourceName) {
  850. $fontDictionary = $fontResources->$fontResourceName;
  851. if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference ||
  852. $fontDictionary instanceof Zend_Pdf_Element_Object) ) {
  853. // Font dictionary has to be an indirect object or object reference
  854. continue;
  855. }
  856. $fontResourcesUnique[$fontDictionary->toString($this->_objFactory)] = $fontDictionary;
  857. }
  858. }
  859. $fonts = array();
  860. require_once 'Zend/Pdf/Exception.php';
  861. foreach ($fontResourcesUnique as $resourceReference => $fontDictionary) {
  862. try {
  863. // Try to extract font
  864. $extractedFont = new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
  865. $fonts[$resourceReference] = $extractedFont;
  866. } catch (Zend_Pdf_Exception $e) {
  867. if ($e->getMessage() != 'Unsupported font type.') {
  868. throw $e;
  869. }
  870. }
  871. }
  872. return $fonts;
  873. }
  874. /**
  875. * Extract font attached to the page by specific font name
  876. *
  877. * $fontName should be specified in UTF-8 encoding
  878. *
  879. * @return Zend_Pdf_Resource_Font_Extracted|null
  880. */
  881. public function extractFont($fontName)
  882. {
  883. $fontResourcesUnique = array();
  884. require_once 'Zend/Pdf/Exception.php';
  885. foreach ($this->pages as $page) {
  886. $pageResources = $page->extractResources();
  887. if ($pageResources->Font === null) {
  888. // Page doesn't contain have any font reference
  889. continue;
  890. }
  891. $fontResources = $pageResources->Font;
  892. foreach ($fontResources->getKeys() as $fontResourceName) {
  893. $fontDictionary = $fontResources->$fontResourceName;
  894. if (! ($fontDictionary instanceof Zend_Pdf_Element_Reference ||
  895. $fontDictionary instanceof Zend_Pdf_Element_Object) ) {
  896. // Font dictionary has to be an indirect object or object reference
  897. continue;
  898. }
  899. $resourceReference = $fontDictionary->toString($this->_objFactory);
  900. if (isset($fontResourcesUnique[$resourceReference])) {
  901. continue;
  902. } else {
  903. // Mark resource as processed
  904. $fontResourcesUnique[$resourceReference] = 1;
  905. }
  906. if ($fontDictionary->BaseFont->value != $fontName) {
  907. continue;
  908. }
  909. try {
  910. // Try to extract font
  911. return new Zend_Pdf_Resource_Font_Extracted($fontDictionary);
  912. } catch (Zend_Pdf_Exception $e) {
  913. if ($e->getMessage() != 'Unsupported font type.') {
  914. throw $e;
  915. }
  916. // Continue searhing
  917. }
  918. }
  919. }
  920. return null;
  921. }
  922. /**
  923. * Render the completed PDF to a string.
  924. * If $newSegmentOnly is true, then only appended part of PDF is returned.
  925. *
  926. * @param boolean $newSegmentOnly
  927. * @param resource $outputStream
  928. * @return string
  929. * @throws Zend_Pdf_Exception
  930. */
  931. public function render($newSegmentOnly = false, $outputStream = null)
  932. {
  933. // Save document properties if necessary
  934. if ($this->properties != $this->_originalProperties) {
  935. $docInfo = $this->_objFactory->newObject(new Zend_Pdf_Element_Dictionary());
  936. foreach ($this->properties as $key => $value) {
  937. switch ($key) {
  938. case 'Trapped':
  939. switch ($value) {
  940. case true:
  941. $docInfo->$key = new Zend_Pdf_Element_Name('True');
  942. break;
  943. case false:
  944. $docInfo->$key = new Zend_Pdf_Element_Name('False');
  945. break;
  946. case null:
  947. $docInfo->$key = new Zend_Pdf_Element_Name('Unknown');
  948. break;
  949. default:
  950. require_once 'Zend/Pdf/Exception.php';
  951. throw new Zend_Pdf_Exception('Wrong Trapped document property vale: \'' . $value . '\'. Only true, false and null values are allowed.');
  952. break;
  953. }
  954. case 'CreationDate':
  955. // break intentionally omitted
  956. case 'ModDate':
  957. $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
  958. break;
  959. case 'Title':
  960. // break intentionally omitted
  961. case 'Author':
  962. // break intentionally omitted
  963. case 'Subject':
  964. // break intentionally omitted
  965. case 'Keywords':
  966. // break intentionally omitted
  967. case 'Creator':
  968. // break intentionally omitted
  969. case 'Producer':
  970. if (extension_loaded('mbstring') === true) {
  971. $detected = mb_detect_encoding($value);
  972. if ($detected !== 'ASCII') {
  973. $value = chr(254) . chr(255) . mb_convert_encoding($value, 'UTF-16', $detected);
  974. }
  975. }
  976. $docInfo->$key = new Zend_Pdf_Element_String((string)$value);
  977. break;
  978. default:
  979. // Set property using PDF type based on PHP type
  980. $docInfo->$key = Zend_Pdf_Element::phpToPdf($value);
  981. break;
  982. }
  983. }
  984. $this->_trailer->Info = $docInfo;
  985. }
  986. $this->_dumpPages();
  987. $this->_dumpNamedDestinations();
  988. // Check, that PDF file was modified
  989. // File is always modified by _dumpPages() now, but future implementations may eliminate this.
  990. if (!$this->_objFactory->isModified()) {
  991. if ($newSegmentOnly) {
  992. // Do nothing, return
  993. return '';
  994. }
  995. if ($outputStream === null) {
  996. return $this->_trailer->getPDFString();
  997. } else {
  998. $pdfData = $this->_trailer->getPDFString();
  999. while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) {
  1000. $pdfData = substr($pdfData, $byteCount);
  1001. }
  1002. return '';
  1003. }
  1004. }
  1005. // offset (from a start of PDF file) of new PDF file segment
  1006. $offset = $this->_trailer->getPDFLength();
  1007. // Last Object number in a list of free objects
  1008. $lastFreeObject = $this->_trailer->getLastFreeObject();
  1009. // Array of cross-reference table subsections
  1010. $xrefTable = array();
  1011. // Object numbers of first objects in each subsection
  1012. $xrefSectionStartNums = array();
  1013. // Last cross-reference table subsection
  1014. $xrefSection = array();
  1015. // Dummy initialization of the first element (specail case - header of linked list of free objects).
  1016. $xrefSection[] = 0;
  1017. $xrefSectionStartNums[] = 0;
  1018. // Object number of last processed PDF object.
  1019. // Used to manage cross-reference subsections.
  1020. // Initialized by zero (specail case - header of linked list of free objects).
  1021. $lastObjNum = 0;
  1022. if ($outputStream !== null) {
  1023. if (!$newSegmentOnly) {
  1024. $pdfData = $this->_trailer->getPDFString();
  1025. while ( strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false ) {
  1026. $pdfData = substr($pdfData, $byteCount);
  1027. }
  1028. }
  1029. } else {
  1030. $pdfSegmentBlocks = ($newSegmentOnly) ? array() : array($this->_trailer->getPDFString());
  1031. }
  1032. // Iterate objects to create new reference table
  1033. foreach ($this->_objFactory->listModifiedObjects() as $updateInfo) {
  1034. $objNum = $updateInfo->getObjNum();
  1035. if ($objNum - $lastObjNum != 1) {
  1036. // Save cross-reference table subsection and start new one
  1037. $xrefTable[] = $xrefSection;
  1038. $xrefSection = array();
  1039. $xrefSectionStartNums[] = $objNum;
  1040. }
  1041. if ($updateInfo->isFree()) {
  1042. // Free object cross-reference table entry
  1043. $xrefSection[] = sprintf("%010d %05d f \n", $lastFreeObject, $updateInfo->getGenNum());
  1044. $lastFreeObject = $objNum;
  1045. } else {
  1046. // In-use object cross-reference table entry
  1047. $xrefSection[] = sprintf("%010d %05d n \n", $offset, $updateInfo->getGenNum());
  1048. $pdfBlock = $updateInfo->getObjectDump();
  1049. $offset += strlen($pdfBlock);
  1050. if ($outputStream === null) {
  1051. $pdfSegmentBlocks[] = $pdfBlock;
  1052. } else {
  1053. while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
  1054. $pdfBlock = substr($pdfBlock, $byteCount);
  1055. }
  1056. }
  1057. }
  1058. $lastObjNum = $objNum;
  1059. }
  1060. // Save last cross-reference table subsection
  1061. $xrefTable[] = $xrefSection;
  1062. // Modify first entry (specail case - header of linked list of free objects).
  1063. $xrefTable[0][0] = sprintf("%010d 65535 f \n", $lastFreeObject);
  1064. $xrefTableStr = "xref\n";
  1065. foreach ($xrefTable as $sectId => $xrefSection) {
  1066. $xrefTableStr .= sprintf("%d %d \n", $xrefSectionStartNums[$sectId], count($xrefSection));
  1067. foreach ($xrefSection as $xrefTableEntry) {
  1068. $xrefTableStr .= $xrefTableEntry;
  1069. }
  1070. }
  1071. $this->_trailer->Size->value = $this->_objFactory->getObjectCount();
  1072. $pdfBlock = $xrefTableStr
  1073. . $this->_trailer->toString()
  1074. . "startxref\n" . $offset . "\n"
  1075. . "%%EOF\n";
  1076. if ($outputStream === null) {
  1077. $pdfSegmentBlocks[] = $pdfBlock;
  1078. return implode('', $pdfSegmentBlocks);
  1079. } else {
  1080. while ( strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false ) {
  1081. $pdfBlock = substr($pdfBlock, $byteCount);
  1082. }
  1083. return '';
  1084. }
  1085. }
  1086. /**
  1087. * Set the document-level JavaScript
  1088. *
  1089. * @param string $javascript
  1090. */
  1091. public function setJavaScript($javascript)
  1092. {
  1093. $this->_javaScript = $javascript;
  1094. }
  1095. /**
  1096. * Convert date to PDF format (it's close to ASN.1 (Abstract Syntax Notation
  1097. * One) defined in ISO/IEC 8824).
  1098. *
  1099. * @todo This really isn't the best location for this method. It should
  1100. * probably actually exist as Zend_Pdf_Element_Date or something like that.
  1101. *
  1102. * @todo Address the following E_STRICT issue:
  1103. * PHP Strict Standards: date(): It is not safe to rely on the system's
  1104. * timezone settings. Please use the date.timezone setting, the TZ
  1105. * environment variable or the date_default_timezone_set() function. In
  1106. * case you used any of those methods and you are still getting this
  1107. * warning, you most likely misspelled the timezone identifier.
  1108. *
  1109. * @param integer $timestamp (optional) If omitted, uses the current time.
  1110. * @return string
  1111. */
  1112. public static function pdfDate($timestamp = null)
  1113. {
  1114. if ($timestamp === null) {
  1115. $date = date('\D\:YmdHisO');
  1116. } else {
  1117. $date = date('\D\:YmdHisO', $timestamp);
  1118. }
  1119. return substr_replace($date, '\'', -2, 0) . '\'';
  1120. }
  1121. }