PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Pdftk/Pdftk.php

https://github.com/khandieyea/php-pdtfk-toolkit
PHP | 570 lines | 286 code | 72 blank | 212 comment | 39 complexity | e723c461c7f5a19787b2229368f9876c MD5 | raw file
  1. <?php
  2. namespace Pdftk;
  3. use Pdftk\File\Input;
  4. /**
  5. * @author Ben Squire <b.squire@gmail.com>
  6. * @license Apache 2.0
  7. *
  8. * @package PDFTK-PHP-Library
  9. * @version 1.2
  10. *
  11. * @abstract This class allows you to integrate with PDFTK command line from within
  12. * your PHP application (An application for PDF: merging, encrypting, rotating, watermarking,
  13. * metadata viewer/editor, compressing etc etc). This library is currently limited
  14. * to the concatenation functionality of the binary; additional functionality to
  15. * come over time.
  16. *
  17. * This library is in no way connected with the author of PDFTK.
  18. *
  19. * To be able to use this library a working version of the binary must be installed
  20. * and its path configured in config.php.
  21. *
  22. * @uses http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/
  23. *
  24. * @see install.md
  25. *
  26. * @example examples/example1.php
  27. * @example examples/example2.php
  28. * @example examples/example3.php
  29. * @example examples/example4.php
  30. * @example examples/example5.php
  31. * @example examples/example6.php
  32. */
  33. class Pdftk
  34. {
  35. const VERSION = '1.2';
  36. protected $sBinary = '/usr/local/bin/pdftk';
  37. protected $aInputFiles = null;
  38. protected $sOutputFilename = null;
  39. protected $bVerbose = false;
  40. protected $bAskMode = false;
  41. protected $bCompress = true;
  42. protected $sOwnerPassword = null;
  43. protected $sUserPassword = null;
  44. protected $iEncryption = 128;
  45. protected $aAccess = array(
  46. 'Printing' => false,
  47. 'DegradedPrinting' => false,
  48. 'ModifyContents' => false,
  49. 'Assembly' => false,
  50. 'CopyContents' => false,
  51. 'ScreenReaders' => false,
  52. 'ModifyAnnotations' => false,
  53. 'FillIn' => false,
  54. 'AllFeatures' => false
  55. );
  56. protected $sInputData = null; //We'll use this to store the key for the input file.
  57. public function __construct($aParams = array())
  58. {
  59. if (isset($aParams['owner_password'])) {
  60. $this->setOwnerPassword($aParams['owner_password']);
  61. }
  62. if (isset($aParams['user_password'])) {
  63. $this->setUserPassword($aParams['user_password']);
  64. }
  65. if (isset($aParams['encryption_level'])) {
  66. $this->setEncryptionLevel($aParams['encryption_level']);
  67. }
  68. if (isset($aParams['verbose_mode'])) {
  69. $this->setVerboseMode($aParams['verbose_mode']);
  70. }
  71. if (isset($aParams['ask_mode'])) {
  72. $this->setAskMode($aParams['ask_mode']);
  73. }
  74. if (isset($aParams['compress'])) {
  75. $this->setCompress($aParams['compress']);
  76. }
  77. }
  78. /**
  79. * Sets the location of the PDFTK executable
  80. *
  81. * @param $sBinary
  82. * @return $this
  83. * @throws \Exception
  84. */
  85. public function setBinary($sBinary)
  86. {
  87. if (!file_exists($sBinary))
  88. {
  89. throw new \Exception('PDFTK path is incorrect');
  90. }
  91. $this->sBinary = $sBinary;
  92. return $this;
  93. }
  94. /**
  95. * Sets the level of encryption to be used (if owner/user password is specified).
  96. * e.g. $foo->setEncryptionLevel(128);
  97. *
  98. * @param int $iEncryptionLevel
  99. * @throws \Exception
  100. * @return $this
  101. */
  102. public function setEncryptionLevel($iEncryptionLevel = 128)
  103. {
  104. if ((int)$iEncryptionLevel !== 40 && (int)$iEncryptionLevel !== 128) {
  105. throw new \Exception('Encryption should either be 40 or 128 (bit)');
  106. }
  107. $this->iEncryption = (int)$iEncryptionLevel;
  108. return $this;
  109. }
  110. /**
  111. * Returns the level of encryption set.
  112. * e.g. $level = $foo->getEncryptionLevel();
  113. *
  114. * @return int
  115. */
  116. public function getEncryptionLevel()
  117. {
  118. return $this->iEncryption;
  119. }
  120. /**
  121. * Returns the version of PDFTK
  122. * @example $sPdftkVersion = $foo->getPdftkVersion();
  123. *
  124. * @return string
  125. */
  126. public function getPdftkVersion()
  127. {
  128. return $this->_exec($this->sBinary . ' --version | grep ^pdftk | cut -d " " -f2');
  129. }
  130. /**
  131. * Sets the users password for the ouput file
  132. * @example $foo->setUserPassword("bar");
  133. *
  134. * @param string $sPassword
  135. * @return $this
  136. */
  137. public function setUserPassword($sPassword = null)
  138. {
  139. $this->sUserPassword = $sPassword;
  140. return $this;
  141. }
  142. /**
  143. * Retrieves the user-password for the output file
  144. * @example $foo->getUserPassword();
  145. *
  146. * @return string
  147. */
  148. public function getUserPassword()
  149. {
  150. return $this->sUserPassword;
  151. }
  152. /**
  153. * Sets the owners password for the ouput file
  154. * $foo->setOwnerPassword("bar");
  155. *
  156. * @param string $sPassword
  157. * @return $this
  158. */
  159. public function setOwnerPassword($sPassword = null)
  160. {
  161. $this->sOwnerPassword = $sPassword;
  162. return $this;
  163. }
  164. /**
  165. * Retrieves the owner-password for the output file
  166. * @example $foo->getOwnerPassword();
  167. *
  168. * @return string
  169. */
  170. public function getOwnerPassword()
  171. {
  172. return $this->sOwnerPassword;
  173. }
  174. /**
  175. * Sets whether the cli will output verbose information
  176. * @example $foo->setVerboseMode(false);
  177. *
  178. * @param bool $bVerbose
  179. * @throws \Exception
  180. *
  181. * @return $this
  182. */
  183. public function setVerboseMode($bVerbose = false)
  184. {
  185. if (!is_bool($bVerbose)) {
  186. throw new \Exception('Verbose mode should be either true or false');
  187. }
  188. $this->bVerbose = (bool)$bVerbose;
  189. return $this;
  190. }
  191. /**
  192. * Returns whether the cli will output verbose information
  193. * @example $foo->getVerboseMode();
  194. *
  195. * @return boolean
  196. */
  197. public function getVerboseMode()
  198. {
  199. return $this->bVerbose;
  200. }
  201. /**
  202. * Sets whether the cli will ask questions when needed
  203. * @example $foo->setAskMode(false);
  204. *
  205. * @param bool $bAskMode
  206. * @throws \Exception
  207. * @return $this
  208. */
  209. public function setAskMode($bAskMode = false)
  210. {
  211. if (!is_bool($bAskMode)) {
  212. throw new \Exception('Ask Mode should be either true or false');
  213. }
  214. $this->bAskMode = (bool)$bAskMode;
  215. return $this;
  216. }
  217. /**
  218. * Returns whether the cli will output questions (when needed)
  219. * @example $foo->getAskMode();
  220. *
  221. * @return boolean
  222. */
  223. public function getAskMode()
  224. {
  225. return $this->bAskMode;
  226. }
  227. /**
  228. * Setups the output file to be used
  229. * @example $foo->setOutputFile("~/tmp/foo.pdf");
  230. *
  231. * @param string $sFilename
  232. * @return $this
  233. */
  234. public function setOutputFile($sFilename)
  235. {
  236. $this->sOutputFilename = $sFilename;
  237. return $this;
  238. }
  239. /**
  240. * Return the output PDFs file
  241. * @example $foo->getOutputFile();
  242. *
  243. * @return string
  244. */
  245. public function getOutputFile()
  246. {
  247. return $this->sOutputFilename;
  248. }
  249. /**
  250. * Compressed by default which prevents editing in text editors, uncompressed allows editing, but increases filesize
  251. * @example $foo->setCompression(true);
  252. *
  253. * @param bool $bCompression
  254. * @return $this
  255. */
  256. public function setCompress($bCompression = true)
  257. {
  258. $this->bCompress = (bool)$bCompression;
  259. return $this;
  260. }
  261. /**
  262. * Returns whether compressions is currently enabled or disabled
  263. * @example $foo->getCompress();
  264. *
  265. * @return bool
  266. */
  267. public function getCompress()
  268. {
  269. return (bool)$this->bCompress;
  270. }
  271. /**
  272. * Setup an input file, as an object
  273. * e.g. $foo->setInputFile(array("password"=>"foobar"));
  274. *
  275. * @param array $aParams
  276. * @return $this
  277. */
  278. public function setInputFile($aParams = array())
  279. {
  280. if ($aParams instanceof Input) {
  281. $this->aInputFiles[] = $aParams;
  282. } else {
  283. $this->aInputFiles[] = new Input($aParams);
  284. }
  285. return $this;
  286. }
  287. /**
  288. * Returns all of the $this->_input_file array
  289. * e.g. $temp = $foo->getInputFile();
  290. *
  291. * @return mixed array
  292. */
  293. public function getInputFile()
  294. {
  295. return $this->aInputFiles;
  296. }
  297. /**
  298. * Returns command to be executed
  299. *
  300. * @return string
  301. */
  302. public function _getCommand()
  303. {
  304. $aCommand = array();
  305. $aCommand[] = $this->sBinary;
  306. $total_inputs = count($this->aInputFiles);
  307. //Assign each PDF a multi-char handle (pdftk-1.45)
  308. foreach ($this->aInputFiles as $iKey => $oFile) {
  309. if ($oFile->getStreamData() !== null) {
  310. $aCommand[] = "-";
  311. $this->sInputData = $iKey;
  312. } else {
  313. if ($this->getPdftkVersion() >= 1.45) {
  314. $handle = chr(65 + floor($iKey / 26) % 26) . chr(65 + $iKey % 26);
  315. } else {
  316. $handle = chr(65 + $iKey);
  317. }
  318. $aCommand[] = $handle . '=' . escapeshellarg($oFile->getFilename());
  319. }
  320. }
  321. //Put read password in place for each file
  322. //input_pw A=foopass
  323. $aPasswords = array();
  324. foreach ($this->aInputFiles as $iKey => $oFile) {
  325. if ($this->getPdftkVersion() >= 1.45) {
  326. $handle = chr(65 + floor($iKey / 26) % 26) . chr(65 + $iKey % 26);
  327. } else {
  328. $handle = chr(65 + $iKey);
  329. }
  330. if ($oFile->getPassword() !== null) {
  331. $aPasswords[] = $handle . '=' . $oFile->getPassword();
  332. }
  333. }
  334. if (count($aPasswords) > 0) {
  335. $aCommand[] = 'input_pw ' . implode(' ', $aPasswords);
  336. }
  337. // TODO: PDFTK Capable of much more functionality, extend here.
  338. $aCommand[] = 'cat';
  339. //Fetch command for each input file
  340. if ($total_inputs > 0) {
  341. foreach ($this->aInputFiles as $iKey => $oFile) {
  342. if ($this->getPdftkVersion() >= 1.45) {
  343. $handle = chr(65 + floor($iKey / 26) % 26) . chr(65 + $iKey % 26);
  344. } else {
  345. $handle = chr(65 + $iKey);
  346. }
  347. $aCommand[] = $handle . $oFile->_getCatCommand();
  348. }
  349. }
  350. //Output file params
  351. $aCommand[] = 'output';
  352. if (!empty($this->sOutputFilename)) {
  353. $aCommand[] = escapeshellarg($this->sOutputFilename);
  354. } else {
  355. $aCommand[] = '-';
  356. }
  357. //Check for PDF password...
  358. if ($this->sOwnerPassword !== null || $this->sUserPassword !== null) {
  359. //Set Encryption Level
  360. $aCommand[] = 'encrypt_' . $this->iEncryption . 'bit';
  361. //TODO: Sets permissions
  362. //pdftk mydoc.pdf output mydoc.128.pdf owner_pw foo user_pw baz allow printing
  363. //Printing, DegradedPrinting, ModifyContents, Assembly, CopyContents,
  364. //ScreenReaders, ModifyAnnotations, FillIn, AllFeatures
  365. //Setup owner password
  366. if ($this->sOwnerPassword !== null) {
  367. $aCommand[] = 'owner_pw ' . $this->sOwnerPassword;
  368. }
  369. //Setup owner password
  370. if ($this->sUserPassword !== null) {
  371. $aCommand[] = 'user_pw ' . $this->sUserPassword;
  372. }
  373. }
  374. //Compress
  375. $aCommand[] = (($this->bCompress === true) ? 'compress' : 'uncompress');
  376. //Verbose Mode
  377. $aCommand[] = (($this->bVerbose) ? 'verbose' : '');
  378. //Ask Mode
  379. $aCommand[] = (($this->bAskMode) ? 'do_ask' : 'dont_ask');
  380. return implode(' ', $aCommand);
  381. }
  382. /**
  383. * Render document as downloadable resource,
  384. * @example $foo->downloadOutput();
  385. *
  386. * @param boolean $bReturn Should data be returned as well 'echoed'
  387. * @return mixed void|string
  388. */
  389. public function downloadOutput($bReturn = false)
  390. {
  391. $filename = $this->sOutputFilename;
  392. $this->sOutputFilename = null;
  393. $pdfData = $this->_renderPdf();
  394. header('Content-type: application/pdf');
  395. header('Content-Disposition: attachment; filename="' . $filename . '"');
  396. echo $pdfData;
  397. if ($bReturn) {
  398. return $pdfData;
  399. }
  400. }
  401. /**
  402. * Render document as inline resource
  403. * @example $foo->inlineOutput();
  404. *
  405. * @param string $sFilename The filename if your were to save the pdf
  406. * @param boolean $bReturn Whether we should return the pdf in string format as well
  407. * @throws \Exception
  408. * @return type
  409. */
  410. public function inlineOutput($sFilename = 'output.pdf', $bReturn = false)
  411. {
  412. if (strlen($sFilename) === 0 || !is_string($sFilename)) {
  413. throw new \Exception('Invalid output filename');
  414. }
  415. $this->sOutputFilename = null;
  416. $sFilename = preg_replace('/[^a-z0-9_\-.]+/i', '_', str_replace('.pdf', '', strtolower($sFilename))) . '.pdf';
  417. $pdfData = $this->_renderPdf();
  418. header('Content-type: application/pdf');
  419. header('Cache-Control: public, must-revalidate, max-age=0');
  420. header('Content-Disposition: inline; filename="' . $sFilename . '"');
  421. header('Content-Transfer-Encoding: binary');
  422. header('Content-Length: ' . strlen($pdfData));
  423. header('Accept-Ranges: bytes');
  424. echo $pdfData;
  425. if ($bReturn) {
  426. return $pdfData;
  427. }
  428. }
  429. /**
  430. * Builds the final PDF
  431. * @example $foo->_renderPdf();
  432. *
  433. * @throws \Exception
  434. * @return string
  435. */
  436. public function _renderPdf()
  437. {
  438. $sData = ((!is_null($this->sInputData) ? $this->aInputFiles[$this->sInputData]->getStreamData() : null));
  439. $sContent = $this->_exec($this->_getCommand(), $sData);
  440. if (strlen($sContent['stderr']) > 0) {
  441. throw new \Exception('System error: ' . $sContent['stderr']);
  442. }
  443. //Error only if we expecting something from stdout and nothing was returned
  444. if (is_null($this->sOutputFilename) && mb_strlen($sContent['stdout'], 'utf-8') === 0) {
  445. throw new \Exception(
  446. 'PDF-TK did not return any data: ' .
  447. $this->_getCommand() .
  448. ' ' .
  449. $this->aInputFiles[$this->sInputData]->getStreamData()
  450. );
  451. }
  452. if ((int)$sContent['return'] > 1) {
  453. throw new \Exception('Shell error, return code: ' . (int)$sContent['return']);
  454. }
  455. return $sContent['stdout'];
  456. }
  457. /**
  458. * Executes PDFtk command
  459. *
  460. * @param string $sCommand Command to execute
  461. * @param string $sInput Other input (not arguments)??
  462. * @throws \Exception
  463. *
  464. * @return array
  465. */
  466. protected function _exec($sCommand, $sInput = null)
  467. {
  468. //TODO: Better handling of error codes
  469. //http://stackoverflow.com/questions/334879/how-do-i-get-the-application-exit-code-from-a-windows-command-line
  470. $aResult = array('stdout' => '', 'stderr' => '', 'return' => '');
  471. $aDescriptorSpec = array(
  472. 0 => array('pipe', 'r'),
  473. 1 => array('pipe', 'w'),
  474. 2 => array('pipe', 'w')
  475. );
  476. $proc = proc_open($sCommand, $aDescriptorSpec, $aPipes);
  477. if (!is_resource($proc)) {
  478. throw new \Exception('Unable to open command line resource');
  479. }
  480. fwrite($aPipes[0], $sInput);
  481. fclose($aPipes[0]);
  482. $aResult['stdout'] = stream_get_contents($aPipes[1]);
  483. fclose($aPipes[1]);
  484. $aResult['stderr'] = stream_get_contents($aPipes[2]);
  485. fclose($aPipes[2]);
  486. $aResult['return'] = proc_close($proc);
  487. return $aResult;
  488. }
  489. /**
  490. * Returns the command to be executed
  491. *
  492. * @return string
  493. */
  494. public function __toString()
  495. {
  496. return $this->_getCommand();
  497. }
  498. }