Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PDF Exports: New command option and library/option cleanup #4969

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example.complete
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace BookStack\Exceptions;

class PdfExportException extends \Exception
{
}
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
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