Skip to content

Commit

Permalink
Merge pull request #4969 from BookStackApp/pdf_command_option
Browse files Browse the repository at this point in the history
PDF Exports: New command option and library/option cleanup
  • Loading branch information
ssddanbrown committed Apr 26, 2024
2 parents d949b97 + f0dd33c commit e1149a2
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 246 deletions.
8 changes: 8 additions & 0 deletions .env.example.complete
Expand Up @@ -325,6 +325,14 @@ FILE_UPLOAD_SIZE_LIMIT=50
# Can be 'a4' or 'letter'.
EXPORT_PAGE_SIZE=a4

# Export PDF Command
# Set a command which can be used to convert a HTML file into a PDF file.
# When false this will not be used.
# String values represent the command to be called for conversion.
# Supports '{input_html_path}' and '{output_pdf_path}' placeholder values.
# Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}"
EXPORT_PDF_COMMAND=false

# Set path to wkhtmltopdf binary for PDF generation.
# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf'
# When false, BookStack will attempt to find a wkhtmltopdf in the application
Expand Down
2 changes: 0 additions & 2 deletions app/Config/app.php
Expand Up @@ -116,8 +116,6 @@
// Application Service Providers
'providers' => ServiceProvider::defaultProviders()->merge([
// Third party service providers
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,

// BookStack custom service providers
Expand Down
42 changes: 27 additions & 15 deletions app/Config/dompdf.php → app/Config/exports.php
@@ -1,23 +1,45 @@
<?php

/**
* DOMPDF configuration options.
* Export configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/

$snappyPaperSizeMap = [
'a4' => 'A4',
'letter' => 'Letter',
];

$dompdfPaperSizeMap = [
'a4' => 'a4',
'letter' => 'letter',
];

$exportPageSize = env('EXPORT_PAGE_SIZE', 'a4');

return [

'show_warnings' => false, // Throw an Exception on warnings from dompdf
// Set a command which can be used to convert a HTML file into a PDF file.
// When false this will not be used.
// String values represent the command to be called for conversion.
// Supports '{input_html_path}' and '{output_pdf_path}' placeholder values.
// Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}"
'pdf_command' => env('EXPORT_PDF_COMMAND', false),

'options' => [
// 2024-04: Snappy/WKHTMLtoPDF now considered deprecated in regard to BookStack support.
'snappy' => [
'pdf_binary' => env('WKHTMLTOPDF', false),
'options' => [
'print-media-type' => true,
'outline' => true,
'page-size' => $snappyPaperSizeMap[$exportPageSize] ?? 'A4',
],
],

'dompdf' => [
/**
* The location of the DOMPDF font directory.
*
Expand Down Expand Up @@ -101,7 +123,7 @@
/**
* Whether to enable font subsetting or not.
*/
'enable_fontsubsetting' => false,
'enable_font_subsetting' => false,

/**
* The PDF rendering backend to use.
Expand Down Expand Up @@ -165,7 +187,7 @@
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
'default_paper_size' => $dompdfPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'a4',
'default_paper_size' => $dompdfPaperSizeMap[$exportPageSize] ?? 'a4',

/**
* The default paper orientation.
Expand Down Expand Up @@ -268,15 +290,6 @@
*/
'font_height_ratio' => 1.1,

/**
* Enable CSS float.
*
* Allows people to disabled CSS float support
*
* @var bool
*/
'enable_css_float' => true,

/**
* Use the HTML5 Lib parser.
*
Expand All @@ -286,5 +299,4 @@
*/
'enable_html5_parser' => true,
],

];
34 changes: 0 additions & 34 deletions app/Config/snappy.php

This file was deleted.

118 changes: 106 additions & 12 deletions app/Entities/Tools/PdfGenerator.php
Expand Up @@ -2,27 +2,28 @@

namespace BookStack\Entities\Tools;

use Barryvdh\DomPDF\Facade\Pdf as DomPDF;
use Barryvdh\Snappy\Facades\SnappyPdf;
use BookStack\Exceptions\PdfExportException;
use Knp\Snappy\Pdf as SnappyPdf;
use Dompdf\Dompdf;
use Symfony\Component\Process\Process;

class PdfGenerator
{
const ENGINE_DOMPDF = 'dompdf';
const ENGINE_WKHTML = 'wkhtml';
const ENGINE_COMMAND = 'command';

/**
* Generate PDF content from the given HTML content.
* @throws PdfExportException
*/
public function fromHtml(string $html): string
{
if ($this->getActiveEngine() === self::ENGINE_WKHTML) {
$pdf = SnappyPDF::loadHTML($html);
$pdf->setOption('print-media-type', true);
} else {
$pdf = DomPDF::loadHTML($html);
}

return $pdf->output();
return match ($this->getActiveEngine()) {
self::ENGINE_COMMAND => $this->renderUsingCommand($html),
self::ENGINE_WKHTML => $this->renderUsingWkhtml($html),
default => $this->renderUsingDomPdf($html)
};
}

/**
Expand All @@ -31,8 +32,101 @@ public function fromHtml(string $html): string
*/
public function getActiveEngine(): string
{
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
if (config('exports.pdf_command')) {
return self::ENGINE_COMMAND;
}

if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) {
return self::ENGINE_WKHTML;
}

return self::ENGINE_DOMPDF;
}

protected function getWkhtmlBinaryPath(): string
{
$wkhtmlBinaryPath = config('exports.snappy.pdf_binary');
if (file_exists(base_path('wkhtmltopdf'))) {
$wkhtmlBinaryPath = base_path('wkhtmltopdf');
}

return $wkhtmlBinaryPath ?: '';
}

protected function renderUsingDomPdf(string $html): string
{
$options = config('exports.dompdf');
$domPdf = new Dompdf($options);
$domPdf->setBasePath(base_path('public'));

$domPdf->loadHTML($this->convertEntities($html));
$domPdf->render();

return (string) $domPdf->output();
}

/**
* @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-');

return $useWKHTML ? self::ENGINE_WKHTML : self::ENGINE_DOMPDF;
$replacementsByPlaceholder = [
'{input_html_path}' => $inputHtml,
'{output_pdf_path}' => $outputPdf,
];

foreach ($replacementsByPlaceholder as $placeholder => $replacement) {
$command = str_replace($placeholder, escapeshellarg($replacement), $command);
}

file_put_contents($inputHtml, $html);

$process = Process::fromShellCommandline($command);
$process->setTimeout(15);
$process->run();

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;
}

protected function renderUsingWkhtml(string $html): string
{
$snappy = new SnappyPdf($this->getWkhtmlBinaryPath());
$options = config('exports.snappy.options');
return $snappy->getOutputFromHtml($html, $options);
}

/**
* 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 = [
'€' => '&euro;',
'£' => '&pound;',
];

foreach ($entities as $search => $replace) {
$subject = str_replace($search, $replace, $subject);
}
return $subject;
}
}
7 changes: 7 additions & 0 deletions app/Exceptions/PdfExportException.php
@@ -0,0 +1,7 @@
<?php

namespace BookStack\Exceptions;

class PdfExportException extends \Exception
{
}
4 changes: 2 additions & 2 deletions composer.json
Expand Up @@ -17,11 +17,11 @@
"ext-mbstring": "*",
"ext-xml": "*",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^2.0",
"barryvdh/laravel-snappy": "^1.0",
"doctrine/dbal": "^3.5",
"dompdf/dompdf": "^2.0",
"guzzlehttp/guzzle": "^7.4",
"intervention/image": "^3.5",
"knplabs/knp-snappy": "^1.5",
"laravel/framework": "^10.10",
"laravel/socialite": "^5.10",
"laravel/tinker": "^2.8",
Expand Down

0 comments on commit e1149a2

Please sign in to comment.