GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

PdfWkhtmltopdfPublisher::setGlobalOptions()   B
last analyzed

Complexity

Conditions 7
Paths 32

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 0
cts 25
cp 0
rs 8.4586
c 0
b 0
f 0
cc 7
nc 32
nop 1
crap 56
1
<?php
2
3
/*
4
 * This file is part of the trefoil application.
5
 *
6
 * (c) Miguel Angel Gabriel <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Trefoil\Publishers;
13
14
use mikehaertl\wkhtmlto\Pdf;
15
use Symfony\Component\Yaml\Yaml;
16
17
/**
18
 * It publishes the book as a PDF file. All the internal links are transformed
19
 * into clickable cross-section book links. These links even display automatically
20
 * the page number where they point into, so no information is lost when printing
21
 * the book.
22
 */
23
class PdfWkhtmltopdfPublisher extends PdfPublisher
24
{
25
    public function checkIfThisPublisherIsSupported()
26
    {
27
        if (null !== $this->app['wkhtmltopdf.path'] && file_exists($this->app['wkhtmltopdf.path'])) {
28
            $wkhtmltopdfPath = $this->app['wkhtmltopdf.path'];
29
        } else {
30
            $wkhtmltopdfPath = $this->findWkhtmltopdfPath();
31
        }
32
33
        $this->app['wkhtmltopdf.path'] = $wkhtmltopdfPath;
34
35
        return null !== $wkhtmltopdfPath && file_exists($wkhtmltopdfPath);
36
    }
37
38
    /**
39
     * Assemble the book using wkhtmltopdf.
40
     *
41
     * Some limitations/remarks:
42
     *
43
     * - Hyphenation is unsupported natively by wkhtmltopdf.
44
     *   Currently managed by an external Javascript library.
45
     *
46
     * - PDF cover is not supported. Only HTML cover is supported. 
47
     * 
48
     * - Facing odd/even pages are unsupported.
49
     *
50
     * - PDF outline cannot be filtered. It will always contain all
51
     *   document headings.
52
     *
53
     * - TOC filtering is (very) limited, performed via XSLT
54
     *   transformation.
55
     *
56
     */
57
    public function assembleBook()
58
    {
59
        // reuse output temp dir for easy debugging and to avoid polluting the cache dir
60
        $tmpDir = $this->app['app.dir.cache'] . '/' . 'easybook_pdf';
61
        if (!$this->app['filesystem']->exists($tmpDir)) {
62
            $this->app['filesystem']->mkdir($tmpDir);
63
        }
64
65
        /** @var Pdf $wkhtmltopdf */
66
        $wkhtmltopdf = $this->app['wkhtmltopdf'];
67
        
68
        // consolidate book images to temp dir
69
        $imagesDir = $tmpDir . '/images';
70
        if (!$this->app['filesystem']->exists($imagesDir)) {
71
            $this->app['filesystem']->mkdir($imagesDir);
72
        }
73
        $this->prepareBookImages($imagesDir);
74
75
        // filter out unusupported content items and extract certain values
76
        $extractedValues = $this->prepareBookItems();
77
78
        // render components
79
        $htmlBookFilePath = $this->renderBook($tmpDir);
80
        $htmlCoverFilePath = $this->renderHtmlCover($tmpDir);
81
        $tocFilePath = $this->renderToc($tmpDir, $extractedValues['toc-title']);
82
83
        // prepare global options like paper size and margins
84
        $globalOptions = $this->setGlobalOptions($tmpDir);
85
86
        // add the stylesheet
87
        $globalOptions = array_merge($globalOptions, $this->prepareStyleSheet($tmpDir));
88
89
        // prepare page options like headers and footers
90
        $pageOptions = $this->prepareHeaderAndFooter($tmpDir);
91
92
        // top and bottom margins need to be tweaked to make room for header/footer
93
        $globalOptions['margin-top'] += $pageOptions['header-spacing'];
94
        $globalOptions['margin-bottom'] += $pageOptions['footer-spacing'];
95
96
        // set the options as global
97
        $wkhtmltopdf->setOptions($globalOptions);
98
99
        // add cover
100
        $wkhtmltopdf->addPage($htmlCoverFilePath);
101
102
        // TOC is always first after cover
103
        $tocOptions = [
104
            'xsl-style-sheet' => $tocFilePath,
105
        ];
106
        $wkhtmltopdf->addToc($tocOptions);
107
108
        // rest of the book
109
        $wkhtmltopdf->addPage($htmlBookFilePath, $pageOptions);
110
111
        // do the conversion
112
        $pdfBookFilePath = $this->app['publishing.dir.output'] . '/book.pdf';
113
114
        if ($wkhtmltopdf->saveAs($pdfBookFilePath) === false) {
115
            $this->displayPdfConversionErrors($wkhtmltopdf->getError());
116
        }
117
118
        echo $wkhtmltopdf->getCommand()->getExecCommand();
119
    }
120
121
    /**
122
     * Looks for the executable of the wkhtmltopdf library.
123
     *
124
     * @return string The absolute path of the executable
125
     *
126
     * @throws \RuntimeException If the wkhtmltopdf executable is not found
127
     */
128 View Code Duplication
    protected function findWkhtmltopdfPath()
129
    {
130
        foreach ($this->app['wkhtmltopdf.default_paths'] as $path) {
131
            if (file_exists($path)) {
132
                return $path;
133
            }
134
        }
135
136
        // the executable couldn't be found in the common
137
        // installation directories. Ask the user for the path
138
        $isInteractive = null !== $this->app['console.input'] && $this->app['console.input']->isInteractive();
139
        if ($isInteractive) {
140
            return $this->askForWkhtmltopdfPath();
141
        }
142
143
        throw new \RuntimeException(
144
            sprintf(
145
                "ERROR: The wkhtmltopdf library needed to generate PDF books cannot be found.\n"
146
                . " Check that you have installed wkhtmltopdf in a common directory \n"
147
                . " or set your custom wkhtmltopdf path in the book's config.yml file:\n\n"
148
                . '%s',
149
                $this->getSampleYamlConfiguration()
150
            )
151
        );
152
    }
153
154
    /**
155
     * Ask the user for the executable location.
156
     *
157
     * @return string
158
     */
159 View Code Duplication
    protected function askForWkhtmltopdfPath()
160
    {
161
        $this->app['console.output']->write(
162
            sprintf(
163
                " In order to generate PDF files, PrinceXML library must be installed. \n\n"
164
                . " We couldn't find PrinceXML executable in any of the following directories: \n"
165
                . "   -> %s \n\n"
166
                . " If you haven't installed it yet, you can download a fully-functional demo at: \n"
167
                . " %s \n\n"
168
                . " If you have installed in a custom directory, please type its full absolute path:\n > ",
169
                implode($this->app['prince.default_paths'], "\n   -> "),
170
                'http://wkhtmltopdf.org/downloads.html'
171
            )
172
        );
173
174
        $userGivenPath = trim(fgets(STDIN));
175
176
        // output a newline for aesthetic reasons
177
        $this->app['console.output']->write("\n");
178
179
        return $userGivenPath;
180
    }
181
182
    /**
183
     * It displays the error messages generated by the PDF conversion
184
     * process in a user-friendly way.
185
     *
186
     * @param array $errorMessages The array of messages generated by PrinceXML
187
     */
188
    protected function displayPdfConversionErrors($errorMessages)
189
    {
190
        $this->app['console.output']->writeln("\n Wkhtmltopdf errors and warnings");
191
        $this->app['console.output']->writeln(" -------------------------------\n");
192
        $this->app['console.output']->writeln($errorMessages);
193
        $this->app['console.output']->writeln("\n");
194
    }
195
196
    /**
197
     * It returns the needed configuration to set up the custom wkhtmltopdf path
198
     * using YAML format.
199
     *
200
     * @return string The sample YAML configuration
201
     */
202
    private function getSampleYamlConfiguration()
203
    {
204
        return <<<YAML
205
  easybook:
206
      parameters:
207
          wkhtmltopdf.path: '/path/to/utils/wkhtmltopdf'
208
209
  book:
210
      title:  ...
211
      author: ...
212
      # ...
213
YAML;
214
    }
215
216
    /**
217
     * Set global wkhtmptopdf options.
218
     *
219
     * @param $tmpDir
220
     *
221
     * @return array options
222
     */
223
    protected function setGlobalOptions($tmpDir)
224
    {
225
        // margins and media size
226
        // TODO: allow other units (inches, cms)
227
        $marginTop = str_replace('mm', '', $this->app->edition('margin')['top']);
228
        $marginBottom = str_replace('mm', '', $this->app->edition('margin')['bottom']);
229
        $marginLeft = str_replace('mm', '', $this->app->edition('margin')['inner']);
230
        $marginRight = str_replace(
231
            'mm',
232
            '',
233
            isset($this->app->edition('margin')['outer']) ?: $this->app->edition('margin')['outter']
234
        );
235
        $orientation = $this->app->edition('orientation') ?: 'portrait';
236
237
        $newOptions = [
238
            'page-size'     => $this->app->edition('page_size'),
239
            'margin-top'    => $marginTop ?: 25,
240
            'margin-bottom' => $marginBottom ?: 25,
241
            'margin-left'   => $marginLeft ?: 30,
242
            'margin-right'  => $marginRight ?: 20,
243
            'orientation'   => $orientation,
244
            'encoding'      => 'UTF-8',
245
            'print-media-type',
246
        ];
247
248
        // misc.
249
        $newOptions['outline-depth'] = $this->app->edition('toc')['deep'];
250
251
        // dump outline xml for easy outline/toc debugging
252
        $newOptions['dump-outline'] = $tmpDir . '/outline.xml';
253
        
254
        return $newOptions;
255
    }
256
257
    /**
258
     * Prepare the stylesheets to use in the book.
259
     *
260
     * @param $tmpDir
261
     *
262
     * @return array $options
263
     */
264
    protected function prepareStyleSheet($tmpDir)
265
    {
266
        $newOptions = [];
267
268
        // copy the general styles if edition wants them included
269 View Code Duplication
        if ($this->app->edition('include_styles')) {
270
            $defaultStyles = $tmpDir . '/default_styles.css';
271
            $this->app->render(
272
                '@theme/wkhtmltopdf-style.css.twig',
273
                array('resources_dir' => $this->app['app.dir.resources'] . '/'),
274
                $defaultStyles
275
            );
276
277
            $newOptions['user-style-sheet'] = $defaultStyles;
278
        }
279
280
        // get the custom templates for the book
281
        $customCss = $this->getCustomCssFile();
282
283
        // concat it to the general styles or set it as default
284
        if (file_exists($customCss)) {
285
            if (isset($newOptions['user-style-sheet'])) {
286
                $customCssText = file_get_contents($customCss);
287
                file_put_contents(
288
                    $newOptions['user-style-sheet'],
289
                    "\n/* --- custom styles --- */\n" . $customCssText,
290
                    FILE_APPEND
291
                );
292
293
            } else {
294
                $newOptions['user-style-sheet'] = $customCss;
295
            }
296
        }
297
298
        return $newOptions;
299
    }
300
301
    /**
302
     * Prepare book items to be rendered, filtering out unuspported types
303
     * and extracting certain values.
304
     *
305
     * @return array options
306
     */
307
    protected function prepareBookItems()
308
    {
309
        $extracted = [];
310
311
        $newItems = [];
312
313
        foreach ($this->app['publishing.items'] as $item) {
314
315
            // extract toc title
316
            if ($item['config']['element'] === 'toc') {
317
                $extracted['toc-title'] = $item['title'];
318
            }
319
320
            // exclude unsupported items
321
            // - toc: added by wkhtmltopdf
322
            // - tof: no way to render with page numbers
323
            // - cover: added after document generation
324
            if (!in_array($item['config']['element'], ['toc', 'tof', 'cover'])) {
325
                $newItems[] = $item;
326
            }
327
        }
328
329
        $this->app['publishing.items'] = $newItems;
330
331
        return $extracted;
332
    }
333
334
    /**
335
     * Render header and footer yml file and configure options.
336
     *
337
     * @param $tmpDir
338
     *
339
     * @return array options
340
     */
341
    protected function prepareHeaderAndFooter($tmpDir)
342
    {
343
        $newOptions = [];
344
345
        $headerFooterFile = $tmpDir . '/header-footer.yml';
346
        $this->app->render(
347
            '@theme/wkhtmltopdf-header-footer.yml.twig',
348
            [],
349
            $headerFooterFile
350
        );
351
352
        $values = Yaml::parse(file_get_contents($headerFooterFile));
353
        
354
        $newOptions['header-spacing'] = $values['header']['spacing'] ?: 20;
355
        $newOptions['header-font-name'] = $values['header']['font-name'] ?: 'sans-serif';
356
        $newOptions['header-font-size'] = $values['header']['font-size'] ?: 12;
357
        $newOptions['header-left'] = $values['header']['left'] ?: '[doctitle]';
358
        $newOptions['header-center'] = $values['header']['center'] ?: '';
359
        $newOptions['header-right'] = $values['header']['right'] ?: '[section]';
360
        if ($values['header']['line'] === true) {
361
            $newOptions[] = 'header-line';
362
        } else {
363
            $newOptions[] = 'no-header-line';
364
        }
365
366
        $newOptions['footer-spacing'] = $values['footer']['spacing'] ?: 20;
367
        $newOptions['footer-font-name'] = $values['footer']['font-name'] ?: 'sans-serif';
368
        $newOptions['footer-font-size'] = $values['footer']['font-size'] ?: 12;
369
        $newOptions['footer-left'] = $values['footer']['left'] ?: '';
370
        $newOptions['footer-center'] = $values['footer']['center'] ?: '[page]';
371
        $newOptions['footer-right'] = $values['footer']['right'] ?: '';
372
        if ($values['footer']['line'] === true) {
373
            $newOptions[] = 'footer-line';
374
        } else {
375
            $newOptions[] = 'no-footer-line';
376
        }
377
378
        return $newOptions;
379
    }
380
381
    /**
382
     * Render the whole book (except excluded items) and set options.
383
     *
384
     * @param $tmpDir
385
     *
386
     * @return string
387
     */
388
    protected function renderBook($tmpDir)
389
    {
390
        $htmlBookFilePath = $tmpDir . '/book.html';
391
        $this->app->render(
392
            'book.twig',
393
            [
394
                'items'         => $this->app['publishing.items'],
395
                'resources_dir' => $this->app['app.dir.resources'] . '/'
396
            ],
397
            $htmlBookFilePath
398
        );
399
400
        return $htmlBookFilePath;
401
    }
402
403
    /**
404
     * Render the cover html file.
405
     *
406
     * @param $tmpDir
407
     *
408
     * @return string
409
     */
410
    protected function renderHtmlCover($tmpDir)
411
    {
412
        $this->app->edition('has_cover_image', false);
413
        
414
        $htmlCoverFilePath = $tmpDir . '/cover.html';
415
        $this->app->render(
416
            'cover.twig',
417
            [],
418
            $htmlCoverFilePath
419
        );
420
421
        return $htmlCoverFilePath;
422
    }
423
424
    /**
425
     * Render the TOC html file.
426
     *
427
     * @param $tmpDir
428
     * @param $tocTitle
429
     *
430
     * @return string
431
     *
432
     */
433
    protected function renderToc($tmpDir, $tocTitle)
434
    {
435
        $tocFilePath = $tmpDir . '/toc.xsl';
436
        $this->app->render(
437
            'wkhtmltopdf-toc.xsl.twig',
438
            [
439
                'toc_title' => $tocTitle, 
440
                'toc_deep'  => $this->app->edition('toc')['deep']
441
            ],
442
            $tocFilePath
443
        );
444
445
        return $tocFilePath;
446
    }
447
}
448