2021-11-25 23:12:32 +08:00
|
|
|
<?php
|
|
|
|
|
2024-10-15 23:14:11 +08:00
|
|
|
namespace BookStack\Exports;
|
2021-11-25 23:12:32 +08:00
|
|
|
|
2024-04-24 23:09:53 +08:00
|
|
|
use BookStack\Exceptions\PdfExportException;
|
2024-04-22 23:40:42 +08:00
|
|
|
use Dompdf\Dompdf;
|
2024-10-15 23:14:11 +08:00
|
|
|
use Knp\Snappy\Pdf as SnappyPdf;
|
2024-09-27 23:33:58 +08:00
|
|
|
use Symfony\Component\Process\Exception\ProcessTimedOutException;
|
2024-04-24 23:09:53 +08:00
|
|
|
use Symfony\Component\Process\Process;
|
2021-11-25 23:12:32 +08:00
|
|
|
|
|
|
|
class PdfGenerator
|
|
|
|
{
|
2022-01-25 01:24:00 +08:00
|
|
|
const ENGINE_DOMPDF = 'dompdf';
|
|
|
|
const ENGINE_WKHTML = 'wkhtml';
|
2024-04-22 23:40:42 +08:00
|
|
|
const ENGINE_COMMAND = 'command';
|
2022-01-25 01:24:00 +08:00
|
|
|
|
2021-11-25 23:12:32 +08:00
|
|
|
/**
|
|
|
|
* Generate PDF content from the given HTML content.
|
2024-04-24 23:09:53 +08:00
|
|
|
* @throws PdfExportException
|
2021-11-25 23:12:32 +08:00
|
|
|
*/
|
|
|
|
public function fromHtml(string $html): string
|
|
|
|
{
|
2024-04-24 23:09:53 +08:00
|
|
|
return match ($this->getActiveEngine()) {
|
|
|
|
self::ENGINE_COMMAND => $this->renderUsingCommand($html),
|
|
|
|
self::ENGINE_WKHTML => $this->renderUsingWkhtml($html),
|
|
|
|
default => $this->renderUsingDomPdf($html)
|
|
|
|
};
|
2021-11-25 23:12:32 +08:00
|
|
|
}
|
2022-01-25 01:24:00 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the currently active PDF engine.
|
|
|
|
* Returns the value of an `ENGINE_` const on this class.
|
|
|
|
*/
|
|
|
|
public function getActiveEngine(): string
|
|
|
|
{
|
2024-04-24 23:09:53 +08:00
|
|
|
if (config('exports.pdf_command')) {
|
|
|
|
return self::ENGINE_COMMAND;
|
|
|
|
}
|
|
|
|
|
2024-04-24 22:13:44 +08:00
|
|
|
if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) {
|
2024-04-22 23:40:42 +08:00
|
|
|
return self::ENGINE_WKHTML;
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::ENGINE_DOMPDF;
|
|
|
|
}
|
|
|
|
|
2024-04-24 22:13:44 +08:00
|
|
|
protected function getWkhtmlBinaryPath(): string
|
|
|
|
{
|
|
|
|
$wkhtmlBinaryPath = config('exports.snappy.pdf_binary');
|
|
|
|
if (file_exists(base_path('wkhtmltopdf'))) {
|
|
|
|
$wkhtmlBinaryPath = base_path('wkhtmltopdf');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $wkhtmlBinaryPath ?: '';
|
|
|
|
}
|
|
|
|
|
2024-04-22 23:40:42 +08:00
|
|
|
protected function renderUsingDomPdf(string $html): string
|
|
|
|
{
|
|
|
|
$options = config('exports.dompdf');
|
|
|
|
$domPdf = new Dompdf($options);
|
|
|
|
$domPdf->setBasePath(base_path('public'));
|
2022-01-25 04:55:03 +08:00
|
|
|
|
2024-04-22 23:40:42 +08:00
|
|
|
$domPdf->loadHTML($this->convertEntities($html));
|
|
|
|
$domPdf->render();
|
|
|
|
|
|
|
|
return (string) $domPdf->output();
|
|
|
|
}
|
|
|
|
|
2024-04-24 23:09:53 +08:00
|
|
|
/**
|
|
|
|
* @throws PdfExportException
|
|
|
|
*/
|
|
|
|
protected function renderUsingCommand(string $html): string
|
|
|
|
{
|
|
|
|
$command = config('exports.pdf_command');
|
|
|
|
$inputHtml = tempnam(sys_get_temp_dir(), 'bs-pdfgen-html-');
|
|
|
|
$outputPdf = tempnam(sys_get_temp_dir(), 'bs-pdfgen-output-');
|
|
|
|
|
|
|
|
$replacementsByPlaceholder = [
|
|
|
|
'{input_html_path}' => $inputHtml,
|
2024-04-26 22:39:40 +08:00
|
|
|
'{output_pdf_path}' => $outputPdf,
|
2024-04-24 23:09:53 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($replacementsByPlaceholder as $placeholder => $replacement) {
|
|
|
|
$command = str_replace($placeholder, escapeshellarg($replacement), $command);
|
|
|
|
}
|
|
|
|
|
|
|
|
file_put_contents($inputHtml, $html);
|
|
|
|
|
2024-09-27 23:33:58 +08:00
|
|
|
$timeout = intval(config('exports.pdf_command_timeout'));
|
2024-04-24 23:09:53 +08:00
|
|
|
$process = Process::fromShellCommandline($command);
|
2024-09-27 23:33:58 +08:00
|
|
|
$process->setTimeout($timeout);
|
|
|
|
|
|
|
|
try {
|
|
|
|
$process->run();
|
|
|
|
} catch (ProcessTimedOutException $e) {
|
|
|
|
throw new PdfExportException("PDF Export via command failed due to timeout at {$timeout} second(s)");
|
|
|
|
}
|
2024-04-24 23:09:53 +08:00
|
|
|
|
|
|
|
if (!$process->isSuccessful()) {
|
|
|
|
throw new PdfExportException("PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}");
|
|
|
|
}
|
|
|
|
|
|
|
|
$pdfContents = file_get_contents($outputPdf);
|
|
|
|
unlink($outputPdf);
|
|
|
|
|
|
|
|
if ($pdfContents === false) {
|
|
|
|
throw new PdfExportException("PDF Export via command failed, unable to read PDF output file");
|
|
|
|
} else if (empty($pdfContents)) {
|
|
|
|
throw new PdfExportException("PDF Export via command failed, PDF output file is empty");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $pdfContents;
|
|
|
|
}
|
|
|
|
|
2024-04-24 22:13:44 +08:00
|
|
|
protected function renderUsingWkhtml(string $html): string
|
|
|
|
{
|
|
|
|
$snappy = new SnappyPdf($this->getWkhtmlBinaryPath());
|
|
|
|
$options = config('exports.snappy.options');
|
|
|
|
return $snappy->getOutputFromHtml($html, $options);
|
|
|
|
}
|
|
|
|
|
2024-04-22 23:40:42 +08:00
|
|
|
/**
|
|
|
|
* Taken from https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/src/PDF.php
|
|
|
|
* Copyright (c) 2021 barryvdh, MIT License
|
|
|
|
* https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/LICENSE
|
|
|
|
*/
|
|
|
|
protected function convertEntities(string $subject): string
|
|
|
|
{
|
|
|
|
$entities = [
|
|
|
|
'€' => '€',
|
|
|
|
'£' => '£',
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($entities as $search => $replace) {
|
|
|
|
$subject = str_replace($search, $replace, $subject);
|
|
|
|
}
|
|
|
|
return $subject;
|
2022-01-25 01:24:00 +08:00
|
|
|
}
|
2021-11-29 05:01:35 +08:00
|
|
|
}
|