1: <?php
2: namespace TIC\DpdfBundle\Service;
3:
4: use TIC\DpdfBundle\Base\PDFService as BaseService;
5:
6: use Symfony\Component\Process\Process;
7: use Symfony\Component\Process\Exception\ProcessFailedException;
8:
9: /**
10: * Service de génération de PDF avec wkhtml(topdf|toimage)
11: * (à partir d'un contenu HMTL ou d'une vue Twig).
12: */
13: class WkhtmlService extends BaseService
14: {
15: protected $cmd_bin;
16: protected $cmd_opt = array();
17: protected $cmd_src;
18: protected $cmd_out;
19: protected $timeout = 60;
20:
21: // ------------------------------------------------------------------------------------------ CONFIG
22:
23: /**
24: * Initialisation de l'objet Dompdf avec ses options.
25: *
26: * @param array $pdf_options Surcharge d'options pour Dompdf
27: * @return WkhtmlService
28: */
29: public function initEngine(array $pdf_options = []): WkhtmlService
30: {
31: $opts = array();
32:
33: // options complétée avec la configuration par défaut (surchargée par parameters)
34: $pdf_options+= $this->confs;
35: if (isset($pdf_options['debug'])) $this->debug = (bool)$pdf_options['debug'];
36:
37: // binary: wkhtmltopdf | wkhtmltoimage
38: switch (\strtolower($pdf_options['generator'])) {
39: case "image":
40: case "png":
41: case "gd":
42: $isPDF = false;
43: $this->cmd_bin = $this->paths['wkhtml_image']; break;
44: default: // auto, pdf...
45: $isPDF = true;
46: $this->cmd_bin = $this->paths['wkhtml_pdf']; break;
47: }
48:
49: // options non disponibles avec wkhtmltoimage
50: if ($isPDF) {
51: // -s, --page-size <Size> Set paper size to: A4, Letter, etc.
52: if ($pdf_options['paper_size'] && $isPDF) {
53: $opts[] = "--page-size";
54: $opts[] = $pdf_options['paper_size'];
55: }
56: // -O, --orientation <orientation> Set orientation to Landscape or Portrait
57: if ($pdf_options['orientation']) {
58: $opts[] = "--orientation";
59: $opts[] = $pdf_options['orientation'];
60: }
61: // --print-media-type Use print media-type instead of screen
62: // --no-print-media-type Do not use print media-type instead of screen (default)
63: if ($pdf_options['media_type'] === 'print') {
64: $opts[] = "--print-media-type";
65: } else {
66: $opts[] = "--no-print-media-type";
67: }
68: // -d, --dpi <dpi> Change the dpi explicitly (this has no effect on X11 based systems) (default 96)
69: // --image-dpi <integer> When embedding images scale them down to this dpi (default 600)
70: if (! empty($pdf_options['pdi']) && \is_numeric($pdf_options['pdi'])) {
71: $opts[] = "--dpi";
72: $opts[] = \intval($pdf_options['pdi']);
73: $opts[] = "--image-dpi";
74: $opts[] = \intval($pdf_options['pdi']);
75: }
76: // --no-background Do not print background
77: if (isset($pdf_options['background']) && empty($pdf_options['background'])) {
78: $opts[] = "--no-background";
79: }
80: // --disable-smart-shrinking Disable the intelligent shrinking strategy used by WebKit that makes the pixel/dpi ratio none constant
81: if (isset($pdf_options['smart_shrinking']) && empty($pdf_options['smart_shrinking'])) {
82: $opts[] = "--disable-smart-shrinking";
83: }
84: // -l, --lowquality Generates lower quality pdf/ps. Useful to shrink the result document space
85: $opts[] = "--lowquality";
86:
87: // @TODO
88: // --viewport-size <> Set viewport size if you have custom scrollbars or css attribute overflow to emulate window size
89: // -B, --margin-bottom <unitreal> Set the page bottom margin
90: // -L, --margin-left <unitreal> Set the page left margin (default 10mm)
91: // -R, --margin-right <unitreal> Set the page right margin (default 10mm)
92: // -T, --margin-top <unitreal> Set the page top margin
93: // --outline Put an outline into the pdf (default)
94: // --no-outline Do not put an outline into the pdf
95: // --outline-depth <level> Set the depth of the outline (default 4)
96: // --footer-html <url> Adds a html footer
97: // --header-html <url> Adds a html header
98: // --replace <name> <value> Replace [name] with value in header and footer (repeatable)
99: // --image-quality <integer> When jpeg compressing images use this quality (default 94)
100: }
101:
102: // options spécifiques à wkhtmltoimage
103: if (! $isPDF) {
104: // -f, --format <format> Output file format
105: $opts[] = "--format";
106: $opts[] = "PNG";
107:
108: // @TODO
109: // --height <int> Set screen height (default is calculated from page content) (default 0)
110: // --crop-h <int> Set height for cropping
111: // --crop-w <int> Set width for cropping
112: // --crop-x <int> Set x coordinate for cropping
113: // --crop-y <int> Set y coordinate for cropping
114: // --quality <int> Output image quality (between 0 and 100)
115: }
116:
117: // --cache-dir <path> Web cache directory
118: if (! empty($this->paths['path_tmpdir'])) {
119: if (! \is_dir($this->paths['path_tmpdir']."/wkweb")) @\mkdir($this->paths['path_tmpdir']."/wkweb", 0750, true);
120: $opts[] = "--cache-dir";
121: $opts[] = $this->paths['path_tmpdir']."/wkweb";
122: }
123: // --disable-local-file-access Do not allowed conversion of a local file to read in other local files, unless explicitly allowed with --allow
124: $opts[] = "--disable-local-file-access";
125: // --allow <path> Allow the file or files from the specified folder to be loaded (repeatable)
126: if (! empty($this->paths['path_source'])) {
127: $opts[] = "--allow";
128: $opts[] = $this->paths['path_source'];
129: }
130: if (! empty($this->paths['root_dir'])) {
131: $opts[] = "--allow";
132: $opts[] = $this->paths['root_dir'];
133: }
134:
135: // -n, --disable-javascript Do not allow web pages to run javascript
136: if (isset($pdf_options['javascript']) && empty($pdf_options['javascript'])) {
137: $opts[] = "--disable-javascript";
138: }
139: // --load-error-handling <handler> Specify how to handle pages that fail to load: abort, ignore or skip (default abort)
140: $opts[] = "--load-error-handling";
141: $opts[] = "ignore";
142: // --load-media-error-handling <handler> Specify how to handle media files that fail to load: abort, ignore or skip (default ignore)
143: $opts[] = "--load-media-error-handling";
144: $opts[] = "ignore";
145: // -q, --quiet Be less verbose
146: $opts[] = "--quiet";
147:
148: // @TODO
149: // --zoom <float> Use this zoom factor (default 1)
150: // --user-style-sheet <url> Specify a user style sheet, to load with
151: // --javascript-delay <msec> Wait some milliseconds for javascript finish (default 200)
152:
153: if ($this->debug) dump($opts);
154:
155: $this->cmd_opt = $opts;
156: return $this;
157: }
158:
159: // ------------------------------------------------------------------------------------------ LOADER
160:
161: /**
162: * Initialisation de l'objet Dompdf à partir d'un fichier HTML.
163: *
164: * @param string $html_file Chemin du fichier HTML à charger
165: * @param array $pdf_options Surcharge d'options pour Dompdf
166: * @return WkhtmlService
167: */
168: public function loadHtmlFile(string $html_file, array $pdf_options = []): WkhtmlService
169: {
170: $this->initEngine($pdf_options);
171: $this->cmd_src = $this->fixHtmlFile($html_file);
172: $this->cmd_out = $this->createTempFile("/wktmp", "out");
173: return $this;
174: }
175:
176: /**
177: * Initialisation de l'objet Dompdf à partir d'un contenu HTML.
178: *
179: * @param string $html_data Contenu du document HTML à charger
180: * @param array $pdf_options Surcharge d'options pour Dompdf
181: * @return WkhtmlService
182: */
183: public function loadHtmlData(string $html_data, array $pdf_options = []): WkhtmlService
184: {
185: $html_file = $this->createTempFile("/wktmp", "html", $html_data);
186: return $this->loadHtmlFile($html_file, $pdf_options);
187: }
188:
189: // ------------------------------------------------------------------------------------------ RENDER
190:
191: /**
192: * Returns the PDF data as a string.
193: *
194: * @param bool $nocompress Désactivation de la compression PDF
195: * @return string Données binaires du document PDF
196: */
197: public function renderData(bool $nocompress = false): string
198: {
199: list($status, $stdout, $stderr) = $this->executeCommand($nocompress);
200: $this->checkCommandOutput($status, $stdout, $stderr, $this->cmd_out);
201: return \file_get_contents($this->cmd_out);
202: }
203:
204: // ------------------------------------------------------------------------------------------ PROCESS
205:
206: /**
207: * Exécution de la commande de génération wkhtmlto(pdf|image).
208: *
209: * @param bool $nocompress Désactivation de la compression PDF
210: * @return array Retour de la commande [status, stdout, stderr]
211: */
212: protected function executeCommand(bool $nocompress = false): array
213: {
214: if (! isset($this->cmd_bin))
215: throw new \LogicException("Initialisation error: wkhtml command is not defined!");
216:
217: $cmd_args = array();
218: if ($nocompress) $cmd_args[] = "--no-pdf-compression";
219: $cmd_args[] = $this->cmd_src;
220: $cmd_args[] = $this->cmd_out;
221:
222: $command = \array_merge([ $this->cmd_bin ], $this->cmd_opt, $cmd_args);
223: $process = new Process($command);
224: if ($this->debug) dump($command, $process->getCommandLine());
225:
226: if (isset($this->timeout)) $process->setTimeout($this->timeout);
227: $process->run();
228:
229: # if (! $process->isSuccessful()) throw new ProcessFailedException($process);
230: return [
231: $process->getExitCode(),
232: $process->getOutput(),
233: $process->getErrorOutput(),
234: ];
235: }
236:
237: // ------------------------------------------------------------------------------------------ CHECKS
238:
239: /**
240: * Vérification du résultat de la commande de génération wkhtmlto(pdf|image).
241: *
242: * @param int $status Command exit status code
243: * @param string $stdout Command stdout content
244: * @param string $stderr Command stderr content
245: * @param string $output Command output fila path
246: * @throws RuntimeException Message d'erreur sur échec de la génération
247: * @return void
248: */
249: protected function checkCommandOutput(int $status, string $stdout, string $stderr, string $output)
250: {
251: if ($this->debug) dump($status, $stdout, $stderr, $output);
252: $dbg = "\n*** stderr:\n%s\n*** stdout:\n%s\n";
253: # if (0 !== $status && '' !== $stderr)
254: # throw new \RuntimeException(\sprintf("Process failed: wkhtml error (%d)".$dbg, $status, $stderr, $stdout));
255: if (! \file_exists($output))
256: throw new \RuntimeException(\sprintf("Process failed: output not exists (%s)".$dbg, $output, $stderr, $stdout));
257: if (! \filesize($output))
258: throw new \RuntimeException(\sprintf("Process failed: output is empty (%s)".$dbg, $output, $stderr, $stdout));
259: }
260:
261: protected function getFileType(string $type = "PDF"): ?array
262: {
263: if (isset($this->cmd_bin) && (\strpos($this->cmd_bin, "image") !== false)) $type = "PNG";
264: return parent::getFileType($type);
265: }
266:
267: }
268: