Overview

Namespaces

  • None
  • PHP
  • WkHtmlToPdf
    • Options

Classes

  • WkHtmlToPdf\WkHtmlToPdf

Interfaces

  • Throwable

Traits

  • WkHtmlToPdf\Options\GlobalOptionsTrait
  • WkHtmlToPdf\Options\HeaderFooterOptionsTrait
  • WkHtmlToPdf\Options\OutlineOptionsTrait
  • WkHtmlToPdf\Options\PageOptionsTrait
  • WkHtmlToPdf\Options\TocOptionsTrait

Exceptions

  • Exception

Functions

  • join_path
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: 
  3: namespace WkHtmlToPdf;
  4: 
  5: use Exception;
  6: use GuzzleHttp\Client;
  7: use GuzzleHttp\Psr7\Uri;
  8: use Symfony\Component\DomCrawler\Crawler;
  9: use Symfony\Component\Process\ProcessBuilder;
 10: 
 11: /**
 12:  * Description of Wkp2p.
 13:  *
 14:  * @author ivanpepelko
 15:  */
 16: class WkHtmlToPdf
 17: {
 18:     use Options\GlobalOptionsTrait,
 19:         Options\OutlineOptionsTrait,
 20:         Options\PageOptionsTrait,
 21:         Options\HeaderFooterOptionsTrait,
 22:         Options\TocOptionsTrait;
 23: 
 24:     private $processBuilder = null;
 25:     private $isQtPatched;
 26:     private $arguments = [];
 27:     private $inputPath;
 28:     private $outputPath;
 29:     private $isPrefetchEnabled = false;
 30:     private $output;
 31:     private $tmpDir;
 32:     private $debug = false;
 33: 
 34:     /**
 35:      * @param string $wkhtml2pdf Path to wkhtmltopdf binary
 36:      * @param string $tmpDir     Path to tmp dir
 37:      * @param bool   $debug
 38:      *
 39:      * @throws Exception
 40:      */
 41:     public function __construct($wkhtml2pdf, $tmpDir = '/tmp', $debug = false)
 42:     {
 43:         if (!is_executable($wkhtml2pdf)) {
 44:             throw new Exception('wkhtml2pdf binary doesn\'t exist or is not executable.');
 45:         }
 46: 
 47:         $this->debug = $debug;
 48:         $this->tmpDir = $tmpDir;
 49: 
 50:         $this->processBuilder = ProcessBuilder::create()
 51:                 ->setPrefix($wkhtml2pdf);
 52: 
 53:         $isQtPatched = false;
 54:         $localpb = clone $this->processBuilder;
 55:         $localpb->add('-V')
 56:                 ->getProcess()
 57:                 ->run(function ($t, $buffer) use ($isQtPatched) {
 58:                     $isQtPatched = strpos($buffer, 'with patched qt') !== false;
 59:                 });
 60:         $this->isQtPatched = $isQtPatched;
 61:     }
 62: 
 63:     /**
 64:      * Set input file path.
 65:      *
 66:      * @param string $path
 67:      *
 68:      * @return WkHtmlToPdf
 69:      */
 70:     public function setInputPath($path)
 71:     {
 72:         $this->inputPath = $path;
 73: 
 74:         return $this;
 75:     }
 76: 
 77:     /**
 78:      * Set input html.
 79:      *
 80:      * @param string $html
 81:      *
 82:      * @return WkHtmlToPdf
 83:      */
 84:     public function setInputHtml($html)
 85:     {
 86:         $tmpFile = join_path($this->tmpDir, md5(time()) . '.html');
 87:         if (!is_writable(dirname($tmpFile))) {
 88:             throw new Exception('Temporary dir is not writable.');
 89:         }
 90:         file_put_contents($tmpFile, $html);
 91:         $this->inputPath = $tmpFile;
 92: 
 93:         return $this;
 94:     }
 95: 
 96:     /**
 97:      * Set output file path.
 98:      *
 99:      * @param string $path
100:      *
101:      * @return WkHtmlToPdf
102:      */
103:     public function setOutputPath($path)
104:     {
105:         $this->outputPath = $path;
106: 
107:         return $this;
108:     }
109: 
110:     /**
111:      * Get pdf as string.
112:      *
113:      * @return string
114:      */
115:     public function getOutput()
116:     {
117:         if (!$this->output) {
118:             throw new Exception('Html is not converted to pdf yet. Call convert() first!');
119:         }
120: 
121:         return $this->output;
122:     }
123: 
124:     /**
125:      * PnP method to get you going ASAP. Sets margins to 6.3mm,
126:      * viewport size to 800x1280, enables low quality and enables media-print if available.
127:      *
128:      * @return WkHtmlToPdf
129:      */
130:     public function useRecommendedOptions()
131:     {
132:         $this->setMarginsAll('6.3mm')
133:                 ->setViewportSize('800x1280')
134:                 ->enableLowQuality()
135:                 ->setLoadErrorHandling('ignore')
136:                 ->setLoadMediaErrorHandling('ignore');
137:         if ($this->isQtPatched) {
138:             $this->enablePrintMediaType();
139:         }
140: 
141:         return $this;
142:     }
143: 
144:     /**
145:      * Do conversion.
146:      *
147:      * @throws Exception
148:      *
149:      * @return WkHtmlToPdf
150:      */
151:     public function convert()
152:     {
153:         $uri = new Uri($this->inputPath);
154:         $isRemote = in_array($uri->getScheme(), ['http', 'https']);
155: 
156:         if (!$isRemote && !is_readable($this->inputPath)) {
157:             throw new Exception('Input file is not readable.');
158:         }
159: 
160:         $outputPath = $this->outputPath ?: join_path($this->tmpDir, md5(time()) . '.pdf');
161: 
162:         if (!is_writable(dirname($outputPath))) {
163:             throw new Exception('Output file is not writable.');
164:         }
165: 
166:         $inputPath = $this->inputPath;
167:         if ($isRemote && $this->isPrefetchEnabled) {
168:             $tmpInput = join_path($this->tmpDir, time() . '.html');
169:             $inputPath = $tmpInput;
170:             // @todo: http options
171:             $combined = $this->combineAssets($this->inputPath);
172:             file_put_contents($tmpInput, $combined);
173:         }
174: 
175:         $pb = $this->processBuilder;
176: 
177:         foreach ($this->arguments as $arg => $value) {
178:             if (is_array($value)) {
179:                 foreach ($value as $name => $val) {
180:                     // if $value is hash create new arg for every value, in form: '--arg name value'
181:                     // if $value is array create new arg for every value, in form: '--arg value'
182:                     $pb->add("--$arg");
183:                     if (is_string($name)) {
184:                         $pb->add($name);
185:                     }
186:                     $pb->add("$val");
187:                 }
188:             } else {
189:                 $pb->add("--$arg");
190:                 if ($value) {
191:                     $pb->add($value);
192:                 }
193:             }
194:         }
195: 
196:         $process = $pb->add($inputPath)
197:                 ->add($outputPath)
198:                 ->getProcess();
199: 
200:         $process->run(function ($t, $buffer) {
201:             if ($this->debug) {
202:                 fwrite(STDERR, $buffer);
203:             }
204:         });
205: 
206:         $this->output = file_get_contents($outputPath);
207:         if (!$this->outputPath) {
208:             unlink($outputPath);
209:         }
210: 
211:         if (isset($tmpInput) && is_writable($tmpInput)) {
212:             unlink($tmpInput);
213:         }
214: 
215:         return $this;
216:     }
217: 
218:     /**
219:      * Prefetch html and feed local file to wkhtmltopdf.
220:      *
221:      * @return WkHtmlToPdf
222:      */
223:     public function enablePrefetch()
224:     {
225:         $this->isPrefetchEnabled = true;
226: 
227:         return $this;
228:     }
229: 
230:     /**
231:      * Disable html prefetching.
232:      *
233:      * @return WkHtmlToPdf
234:      */
235:     public function disablePrefetch()
236:     {
237:         $this->isPrefetchEnabled = false;
238: 
239:         return $this;
240:     }
241: 
242:     /**
243:      * Set argument (works only when constructor param $debug == true).
244:      * All pdf generation related args have their dedicated method.
245:      *
246:      * @param string $arg
247:      * @param mixed  $val
248:      *
249:      * @return WkHtmlToPdf
250:      */
251:     public function setArg($arg, $val = null)
252:     {
253:         if ($this->debug) {
254:             $this->arguments[$arg] = $val;
255:         }
256: 
257:         return $this;
258:     }
259: 
260:     /**
261:      * Unset argument (works only when constructor param $debug == true).
262:      * All pdf generation related args have their dedicated method.
263:      *
264:      * @param string $arg
265:      *
266:      * @return WkHtmlToPdf
267:      */
268:     public function unsetArg($arg)
269:     {
270:         if ($this->debug && array_key_exists($arg, $this->arguments)) {
271:             unset($this->arguments[$arg]);
272:         }
273: 
274:         return $this;
275:     }
276: 
277:     /**
278:      * Replace link[rel=stylesheet], script[src], img and css url() with inline equivalent.
279:      * This method is still WIP and may not give desired results.
280:      *
281:      * @param string $path
282:      *
283:      * @return string Html with inlined resources
284:      */
285:     private function combineAssets($path)
286:     {
287:         $remoteUri = new Uri($path);
288:         $baseUri = $remoteUri->getScheme() . '://' . $remoteUri->getHost();
289:         $client = new Client([
290:             'base_uri' => $baseUri,
291:         ]);
292: 
293:         $htmlContents = $client->get($path)
294:                 ->getBody()
295:                 ->getContents();
296: 
297:         $crawler = new Crawler();
298:         $crawler->addHtmlContent($htmlContents);
299: 
300:         $crawler->filter('link')->each(function ($node) use ($client) {
301:             /* @var $node Crawler */
302: 
303:             if ('stylesheet' !== $node->attr('rel')) {
304:                 return;
305:             }
306: 
307:             $href = $node->attr('href');
308: 
309:             $newNodeContents = $client
310:                     ->get($href)
311:                     ->getBody()
312:                     ->getContents();
313: 
314:             //$url = '#\b(([\w-]+:\/\/?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/)))#iS';
315:             preg_replace_callback("/url\(([A-Za-z0-9\:\.\-_\/]+)\);/", function ($matches) use ($client, $href) {
316:                 try {
317:                     $resource = $client->get(dirname($href) . DIRECTORY_SEPARATOR . end($matches));
318:                     $mime = $resource->getHeader('Content-Type')[0];
319: 
320:                     return sprintf('url(data:%s;base64,%s)', $mime, base64_encode($resource->getBody()->getContents()));
321:                 } catch (Exception $e) {
322:                     //just continue as if nothing happened
323:                 }
324:             }, $newNodeContents);
325: 
326:             $oldNode = $node->getNode(0);
327:             $newNode = $oldNode
328:                     ->ownerDocument
329:                     ->createElement('style', $newNodeContents);
330: 
331:             $node->parents()
332:                     ->getNode(0)
333:                     ->replaceChild($newNode, $oldNode);
334:         });
335: 
336:         $crawler->filter('script')->each(function ($node) use ($client) {
337:             /* @var $node Crawler */
338: 
339:             $src = $node->attr('src');
340: 
341:             if (trim($node->text()) || !$src) {
342:                 $isGa = false !== strpos($node->text(), 'GoogleAnalyticsObject');
343: 
344:                 if ($isGa) {
345:                     $nativeNode = $node->getNode(0);
346:                     $node->getNode(0)->parentNode->removeChild($nativeNode);
347:                 }
348: 
349:                 return;
350:             }
351: 
352:             $newNodeContents = $client
353:                     ->get($src)
354:                     ->getBody()
355:                     ->getContents();
356: 
357:             $oldNode = $node->getNode(0);
358:             $newNode = $oldNode
359:                     ->ownerDocument
360:                     ->createElement('script', htmlspecialchars($newNodeContents));
361: 
362:             $node->parents()
363:                     ->getNode(0)
364:                     ->replaceChild($newNode, $oldNode);
365:         });
366: 
367:         /* something should be done with fonts *//*
368:           $fonts = $client
369:           ->get('http://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js')
370:           ->getBody()
371:           ->getContents();
372: 
373:           $firstScript = $crawler->filter('script')->getNode(0);
374:           $fontLoader = $crawler->getNode(0)->ownerDocument->createElement('script', $fonts);
375:           $firstScript->parentNode->insertBefore($fontLoader, $firstScript);
376:          */
377: 
378:         $crawler->filter('img')->each(function ($node) use ($client) {
379:             /* @var $node Crawler */
380: 
381:             $src = $node->attr('src');
382: 
383:             $resource = $client->get($src);
384:             $mime = $resource->getHeader('Content-Type')[0];
385:             $contents = $resource->getBody()->getContents();
386: 
387:             $node->getNode(0)->setAttribute('src', sprintf('url(data:%s;base64,%s)', $mime, base64_encode($contents)));
388:         });
389: 
390:         return $crawler->html();
391:     }
392: }
393: 
wkp2p API documentation generated by ApiGen