Skip to content

Instantly share code, notes, and snippets.

@fprochazka
Created May 12, 2013 18:33
Show Gist options
  • Select an option

  • Save fprochazka/5564471 to your computer and use it in GitHub Desktop.

Select an option

Save fprochazka/5564471 to your computer and use it in GitHub Desktop.
WkhtmlToPDF abstraction that can run either localy or remotely, or even can be used as a hosted service.
<?php
/**
* This file is part of the Kdyby (http://www.kdyby.org)
*
* Copyright (c) 2008, 2012 Filip Procházka (filip@prochazka.su)
*
* For the full copyright and license information, please view the file license.txt that was distributed with this source code.
*/
namespace Pdf\DI;
use Kdyby;
use Kdyby\Curl\Request;
use Kdyby\Curl\CurlException;
use Nette;
use Nette\Http\Url;
use Nette\Diagnostics\Debugger;
use Pdf\InvalidStateException;
/**
* @author Filip Procházka <filip@prochazka.su>
*
* @property \Nette\Http\Url $codeUrl
* @property \Nette\Http\Url $pingUrl
*/
class Configuration extends Nette\Object
{
/**
* @var string
*/
public $provider;
/**
* @var string
*/
public $apiKey;
/**
* @var string
*/
public $tempDir;
/**
* @return \Nette\Http\Url
*/
public function getCodeUrl()
{
return new Url($this->provider . '/code');
}
/**
* @return \Nette\Http\Url
*/
protected function getPingUrl()
{
return new Url($this->provider . '/ping');
}
/**
* @return bool
*/
public function testConnection()
{
try {
$request = new Request($this->getPingUrl());
$request->headers['X-ApiKey'] = $this->apiKey;
$response = $request->get();
if ($response->getResponse() !== 'pong') {
throw new InvalidStateException($response->getResponse(), $response->headers['Status-Code']);
}
return TRUE;
} catch (CurlException $e) {
Debugger::log($e, 'curl');
return FALSE;
} catch (InvalidStateException $e) {
Debugger::log($e, 'Pdf');
return FALSE;
}
}
}
<?php
namespace Pdf;
use Kdyby;
use Nette;
/**
* @author Filip Procházka <filip@prochazka.su>
*/
interface Exception
{
}
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class IOException extends \RuntimeException implements Exception
{
}
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class InvalidStateException extends \RuntimeException implements Exception
{
}
<?php
namespace Pdf;
use Kdyby;
use Nette;
/**
* @author Filip Procházka <filip@prochazka.su>
*/
interface IGenerator
{
/**
* @param string $html
* @return Nette\Application\IResponse
*/
function render($html);
}
<?php
/**
* This file is part of the Kdyby (http://www.kdyby.org)
*
* Copyright (c) 2008, 2012 Filip Procházka (filip@prochazka.su)
*
* For the full copyright and license information, please view the file license.txt that was distributed with this source code.
*/
namespace Pdf\DI;
use Kdyby;
use Nette;
use Nette\Utils\Validators;
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class PdfExtension extends Nette\Config\CompilerExtension
{
/**
* @var array
*/
public $defaults = array(
'provider' => 'http://pdf.kdyby.org',
'apiKey' => NULL,
);
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$config = $this->getConfig($this->defaults);
$builder->addDefinition($this->prefix('config'))
->setClass('Pdf\DI\Configuration')
->addSetup('$tempDir', array($builder->expand('%tempDir%/pdf')))
->addSetup('$provider', array($config['provider']))
->addSetup('$apiKey', array($config['apiKey']));
$generator = $builder->addDefinition($this->prefix('generator'))
->setClass('Pdf\IGenerator');
if (!empty($config['apiKey'])) {
$generator->setFactory('Pdf\RemoteGenerator');
} else {
$generator->setFactory('Pdf\WkhtmltopdfGenerator');
}
}
public function beforeCompile()
{
$config = $this->getConfig($this->defaults);
if (!empty($config['apiKey'])) {
$configuration = new Configuration();
$configuration->apiKey = $config['apiKey'];
$configuration->provider = $config['provider'];
if ($configuration->testConnection() === FALSE) {
trigger_error("The Pdf generator is badly configured or components are missing. Please check log.", E_USER_WARNING);
}
}
}
/**
* @param \Nette\Config\Configurator $configurator
*/
public static function register(Nette\Config\Configurator $configurator)
{
$configurator->onCompile[] = function ($config, Nette\Config\Compiler $compiler) {
$compiler->addExtension('pdfGenerator', new PdfExtension());
};
}
}
<?php
/**
* This file is part of the Kdyby (http://www.kdyby.org)
*
* Copyright (c) 2008, 2012 Filip Procházka (filip@prochazka.su)
*
* For the full copyright and license information, please view the file license.txt that was distributed with this source code.
*/
namespace Pdf;
use Kdyby;
use Nette;
use Kdyby\Curl\CurlSender;
use Kdyby\Curl\Request;
use Nette\Application\Responses\TextResponse;
use Nette\Diagnostics\Debugger;
use Nette\Utils\Json;
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class RemoteGenerator extends Nette\Object implements IGenerator
{
/**
* @var \Kdyby\QrEncode\DI\Configuration
*/
private $config;
/**
* @var \Kdyby\Curl\CurlSender
*/
private $curlSender;
/**
* @param \Kdyby\QrEncode\DI\Configuration $config
* @param \Kdyby\Curl\CurlSender $curlSender
*/
public function __construct(DI\Configuration $config, CurlSender $curlSender = NULL)
{
$this->config = $config;
$this->curlSender = $curlSender ?: new CurlSender();
}
/**
* @param string $html
* @return \Nette\Application\IResponse|\Nette\Application\Responses\TextResponse
* @throws InvalidStateException
*/
public function render($html)
{
$request = new Request($this->config->getCodeUrl());
$request->headers['X-ApiKey'] = $this->config->apiKey;
$request->setTimeout(45);
$request->setSender($this->curlSender);
try {
$response = $request->post(array('body' => (string) $html));
if ($response->headers['Content-Type'] === 'application/pdf') {
return new TextResponse($response->getResponse());
}
$json = Json::decode($response->getResponse(), Json::FORCE_ARRAY);
throw new InvalidStateException($json['error']);
} catch (Kdyby\Curl\Exception $e) {
throw new InvalidStateException($e->getMessage(), $e->getCode(), $e);
}
}
}
<?php
namespace Pdf;
use DOMDocument;
use Kdyby;
use Nette\Utils\Json;
use Nette\Utils\Strings;
use Nette;
use Wkhtmltopdf\Document;
/**
* @author Filip Procházka <filip@prochazka.su>
*/
class WkhtmltopdfGenerator extends Nette\Object implements IGenerator
{
/**
* @var string
*/
private $tempDir;
/**
* @param string $tempDir
*/
public function __construct(DI\Configuration $config)
{
if (!is_dir($config->tempDir)) {
umask(0);
mkdir($config->tempDir, 0777);
}
$this->tempDir = $config->tempDir;
}
/**
* @param string $html
* @return \Wkhtmltopdf\Document|\Nette\Application\IResponse
*/
public function render($html)
{
$pdf = new Document($this->tempDir);
$pdf->addHtml($html = (string) $html);
$this->loadOptionsFromBody($pdf, $html);
return $pdf;
}
/**
* @param \Wkhtmltopdf\Document|\Wkhtmltopdf\PageMeta $pdf
* @param string $html
* @throws \Nette\MemberAccessException
*/
private function loadOptionsFromBody($pdf, $html)
{
$dom = new DomDocument('1.0', 'UTF-8');
$dom->resolveExternals = FALSE;
$dom->validateOnParse = FALSE;
$dom->preserveWhiteSpace = FALSE;
$dom->strictErrorChecking = FALSE;
$dom->recover = TRUE;
@$dom->loadHTML($html);
$propertyName = function ($name) use ($pdf) {
foreach (array_keys(get_object_vars($pdf)) as $propertyName) {
if (strcasecmp($name, $propertyName) == 0) {
return $propertyName;
}
}
throw new Nette\MemberAccessException("Cannot write to an undeclared property " . get_class($pdf) . "::\$$name.");
};
foreach ($dom->getElementsByTagName('body') as $body) {
/** @var \DOMElement $body */
foreach ($body->attributes as $attrName => $value) {
/** @var \DOMAttr $value */
if (stripos($attrName, 'data-pdf-') !== 0) {
continue;
}
$property = $propertyName(substr($attrName, 9));
if (in_array(substr($value->value, 0, 1), array('[', '{'))) {
$pdf->$property = Json::decode($value->value);
} elseif ($value->value == 'false') {
$pdf->$property = FALSE;
} elseif ($value->value == 'true') {
$pdf->$property = TRUE;
} else {
$pdf->$property = $value->value;
}
}
break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment