Gentics Portal.Node PHP API
 All Classes Namespaces Functions Variables Pages
WkHtmlToPdf.php
1 <?php
2 /**
3  * WkHtmlToPdf
4  *
5  * This class is a slim wrapper around wkhtmltopdf.
6  *
7  * It provides a simple and clean interface to ease PDF creation with wkhtmltopdf.
8  * The wkhtmltopdf binary must be installed and working on your system. The static
9  * binary is preferred but this class should also work with the non static version,
10  * even though a lot of features will be missing.
11  *
12  * Basic use
13  * ---------
14  *
15  * $pdf = new WkHtmlToPdf;
16  *
17  * // Add a HTML file, a HTML string, a page from URL or a PDF file
18  * $pdf->addPage('/home/joe/page.html');
19  * $pdf->addPage('<html>....</html>');
20  * $pdf->addPage('http://google.com');
21  * $pdf->addPage('/home/joe/my.pdf');
22  *
23  * // Add a cover (same sources as above are possible)
24  * $pdf->addCover('mycover.pdf');
25  *
26  * // Add a Table of contents
27  * $pdf->addToc();
28  *
29  * // Save the PDF, or ...
30  * $pdf->saveAs('/tmp/new.pdf');
31  *
32  * // ... send to client for inline display or ...
33  * $pdf->send();
34  *
35  * // ... send to client as file download
36  * $pdf->send('test.pdf');
37  *
38  *
39  * Setting options
40  * ---------------
41  *
42  * The wkhtmltopdf binary has some global options (e.g. to set the document's DPI) and options
43  * for each PDF page (e.g. to supply a custom CSS file). Please see "wkhtmltopdf -H" to get a
44  * list of all available options.
45  *
46  * In addition this class also supports global page options: You can set default page options
47  * that will be applied to every page you add. You can also override these defaults per page:
48  *
49  * $pdf = new WkHtmlToPdf($options); // Set global PDF options
50  * $pdf->setOptions($options); // Set global PDF options (alternative)
51  * $pdf->setPageOptions($options); // Set default page options
52  * $pdf->addPage($page, $options); // Add page with options (overrides default page options)
53  *
54  *
55  * Special global options
56  * ----------------------
57  *
58  * You can use these special global options to set up some file paths:
59  *
60  * bin: path to the wkhtmltopdf binary. Defaults to /usr/bin/wkhtmltopdf.
61  * tmp: path to tmp directory. Defaults to PHP temp dir.
62  *
63  *
64  * Error handling
65  * --------------
66  *
67  * saveAs() and save() will return false on error. In this case the detailed error message
68  * from wkhtmltopdf can be obtained through getError().
69  *
70  * @author Michael Härtl <haertl.mike@gmail.com> (sponsored by PeoplePerHour.com)
71  * @version 1.1.0
72  * @license http://www.opensource.org/licenses/MIT
73  */
75 {
76  protected $bin = '/usr/bin/wkhtmltopdf';
77 
78  protected $options = array();
79  protected $pageOptions = array();
80  protected $objects = array();
81 
82  protected $tmp;
83  protected $tmpFile;
84  protected $tmpFiles = array();
85 
86  protected $error;
87 
88  // Regular expression to detect HTML strings
89  const REGEX_HTML = '/<.*html.*>/i';
90 
91  /**
92  * @param array $options global options for wkhtmltopdf (optional)
93  */
94  public function __construct($options=array())
95  {
96  if($options!==array())
97  $this->setOptions($options);
98  }
99 
100  /**
101  * Remove temporary PDF file and pages when script completes
102  */
103  public function __destruct()
104  {
105  if($this->tmpFile!==null)
106  unlink($this->tmpFile);
107 
108  foreach($this->tmpFiles as $tmp)
109  unlink($tmp);
110  }
111 
112  /**
113  * Add a page object to the output
114  *
115  * @param string $input either a URL, a HTML string or a PDF/HTML filename
116  * @param array $options optional options for this page
117  */
118  public function addPage($input,$options=array())
119  {
120  $options['input'] = preg_match(self::REGEX_HTML, $input) ? $this->createTmpFile($input) : $input;
121  $this->objects[] = array_merge($this->pageOptions,$options);
122  }
123 
124  /**
125  * Add a cover page object to the output
126  *
127  * @param string $input either a URL or a PDF filename
128  * @param array $options optional options for this page
129  */
130  public function addCover($input,$options=array())
131  {
132  $options['input'] = "cover $input";
133  $this->objects[] = array_merge($this->pageOptions,$options);
134  }
135 
136  /**
137  * Add a TOC object to the output
138  *
139  * @param array $options optional options for the table of contents
140  */
141  public function addToc($options=array())
142  {
143  $options['input'] = "toc";
144  $this->objects[] = $options;
145  }
146 
147  /**
148  * Save the PDF to given filename (triggers PDF creation)
149  *
150  * @param string $filename to save PDF as
151  * @return bool wether PDF was created successfully
152  */
153  public function saveAs($filename)
154  {
155  if(($pdfFile = $this->getPdfFilename())===false)
156  return false;
157 
158  copy($pdfFile,$filename);
159  return true;
160  }
161 
162  /**
163  * Send PDF to client, either inline or as download (triggers PDF creation)
164  *
165  * @param mixed $filename the filename to send. If empty, the PDF is streamed.
166  * @return bool wether PDF was created successfully
167  */
168  public function send($filename=null)
169  {
170  if(($pdfFile = $this->getPdfFilename())===false)
171  return false;
172 
173  header('Pragma: public');
174  header('Expires: 0');
175  header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
176  header('Content-Type: application/pdf');
177  header('Content-Transfer-Encoding: binary');
178  header('Content-Length: '.filesize($pdfFile));
179 
180  if($filename!==null)
181  header("Content-Disposition: attachment; filename=\"$filename\"");
182 
183  readfile($pdfFile);
184  return true;
185  }
186 
187  /**
188  * Set global option(s)
189  *
190  * @param array $options list of global options to set as name/value pairs
191  */
192  public function setOptions($options)
193  {
194  foreach($options as $key=>$val)
195  if($key==='bin')
196  $this->bin = $val;
197  elseif($key==='tmp')
198  $this->tmp = $val;
199  elseif(is_int($key))
200  $this->options[] = $val;
201  else
202  $this->options[$key] = $val;
203  }
204 
205  /**
206  * @param array $options that should be applied to all pages as name/value pairs
207  */
208  public function setPageOptions($options=array())
209  {
210  $this->pageOptions = $options;
211  }
212 
213  /**
214  * @return mixed the detailled error message including the wkhtmltopdf command or null if none
215  */
216  public function getError()
217  {
218  return $this->error;
219  }
220 
221  /**
222  * @return string path to temp directory
223  */
224  public function getTmpDir()
225  {
226  if($this->tmp===null)
227  $this->tmp = sys_get_temp_dir();
228 
229  return $this->tmp;
230  }
231 
232  /**
233  * @return mixed the temporary PDF filename or false on error (triggers PDf creation)
234  */
235  protected function getPdfFilename()
236  {
237  if($this->tmpFile===null)
238  {
239  $tmpFile = tempnam($this->getTmpDir(),'tmp_WkHtmlToPdf_');
240 
241  if($this->createPdf($tmpFile)===true)
242  $this->tmpFile = $tmpFile;
243  else
244  return false;
245  }
246 
247  return $this->tmpFile;
248  }
249 
250  /**
251  * @param string $filename the filename of the output file
252  * @return string the wkhtmltopdf command string
253  */
254  protected function getCommand($filename)
255  {
256  $command = $this->bin;
257 
258  $command .= $this->renderOptions($this->options);
259 
260  foreach($this->objects as $object)
261  {
262  $command .= ' '.$object['input'];
263  unset($object['input']);
264  $command .= $this->renderOptions($object);
265  }
266 
267  return $command.' '.$filename;
268  }
269 
270  /**
271  * Create the temporary PDF file
272  */
273  protected function createPdf($fileName)
274  {
275  $command = $this->getCommand($fileName);
276 
277  // we use proc_open with pipes to fetch error output
278  $descriptors = array(
279  1 => array('pipe','w'),
280  2 => array('pipe','w'),
281  );
282  $process = proc_open($command, $descriptors, $pipes);
283 
284  if(is_resource($process)) {
285 
286  $stdout = stream_get_contents($pipes[1]);
287  $stderr = stream_get_contents($pipes[2]);
288  fclose($pipes[1]);
289  fclose($pipes[2]);
290 
291  $result = proc_close($process);
292 
293  if($result!==0)
294  $this->error = "Could not run command $command:\n$stderr";
295  } else
296  $this->error = "Could not run command $command";
297 
298  return $this->error===null;
299  }
300 
301  /**
302  * Create a tmp file with given content
303  *
304  * @param string $content the file content
305  * @return string the path to the created file
306  */
307  protected function createTmpFile($content)
308  {
309  $tmpFile = tempnam($this->getTmpDir(),'tmp_WkHtmlToPdf_');
310  rename($tmpFile, ($tmpFile.='.html'));
311  file_put_contents($tmpFile, $content);
312 
313  $this->tmpFiles[] = $tmpFile;
314 
315  return $tmpFile;
316  }
317 
318  /**
319  * @param array $options for a wkhtml, either global or for an object
320  * @return string the string with options
321  */
322  protected function renderOptions($options)
323  {
324  $out = '';
325  foreach($options as $key=>$val)
326  if(is_numeric($key))
327  $out .= " --$val";
328  else
329  $out .= " --$key $val";
330 
331  return $out;
332  }
333 }