2021-11-25 15:12:32 +00:00
< ? php
2024-10-15 16:14:11 +01:00
namespace BookStack\Exports ;
2021-11-25 15:12:32 +00:00
2024-04-24 16:09:53 +01:00
use BookStack\Exceptions\PdfExportException ;
2024-04-22 16:40:42 +01:00
use Dompdf\Dompdf ;
2026-04-20 15:42:28 +01:00
use FontLib\Font ;
use Illuminate\Support\Str ;
2024-10-15 16:14:11 +01:00
use Knp\Snappy\Pdf as SnappyPdf ;
2024-09-27 16:33:58 +01:00
use Symfony\Component\Process\Exception\ProcessTimedOutException ;
2024-04-24 16:09:53 +01:00
use Symfony\Component\Process\Process ;
2021-11-25 15:12:32 +00:00
class PdfGenerator
{
2022-01-24 17:24:00 +00:00
const ENGINE_DOMPDF = 'dompdf' ;
const ENGINE_WKHTML = 'wkhtml' ;
2024-04-22 16:40:42 +01:00
const ENGINE_COMMAND = 'command' ;
2022-01-24 17:24:00 +00:00
2021-11-25 15:12:32 +00:00
/**
* Generate PDF content from the given HTML content .
2024-04-24 16:09:53 +01:00
* @ throws PdfExportException
2021-11-25 15:12:32 +00:00
*/
public function fromHtml ( string $html ) : string
{
2024-04-24 16:09:53 +01:00
return match ( $this -> getActiveEngine ()) {
self :: ENGINE_COMMAND => $this -> renderUsingCommand ( $html ),
self :: ENGINE_WKHTML => $this -> renderUsingWkhtml ( $html ),
default => $this -> renderUsingDomPdf ( $html )
};
2021-11-25 15:12:32 +00:00
}
2022-01-24 17:24:00 +00:00
/**
* Get the currently active PDF engine .
* Returns the value of an `ENGINE_` const on this class .
*/
public function getActiveEngine () : string
{
2024-04-24 16:09:53 +01:00
if ( config ( 'exports.pdf_command' )) {
return self :: ENGINE_COMMAND ;
}
2024-04-24 15:13:44 +01:00
if ( $this -> getWkhtmlBinaryPath () && config ( 'app.allow_untrusted_server_fetching' ) === true ) {
2024-04-22 16:40:42 +01:00
return self :: ENGINE_WKHTML ;
}
return self :: ENGINE_DOMPDF ;
}
2024-04-24 15:13:44 +01: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 16:40:42 +01:00
protected function renderUsingDomPdf ( string $html ) : string
{
$options = config ( 'exports.dompdf' );
$domPdf = new Dompdf ( $options );
$domPdf -> setBasePath ( base_path ( 'public' ));
2022-01-24 20:55:03 +00:00
2026-04-20 15:42:28 +01:00
$fontMetrics = $domPdf -> getFontMetrics ();
$userFontfamilies = $this -> getUserDomPdfFontFamilies ();
foreach ( $userFontfamilies as $fontFamily => $fonts ) {
2026-04-22 13:22:20 +01:00
try {
$fontMetrics -> setFontFamily ( $fontFamily , $fonts );
} catch ( \Exception $exception ) {
$expectedPath = storage_path ( 'fonts/dompdf' );
throw new PdfExportException ( " Failed to create required font data in { $expectedPath } , Ensure all content in this location is writable by the web server " );
}
2026-04-20 15:42:28 +01:00
}
2024-04-22 16:40:42 +01:00
$domPdf -> loadHTML ( $this -> convertEntities ( $html ));
$domPdf -> render ();
return ( string ) $domPdf -> output ();
}
2026-04-20 15:42:28 +01:00
/**
* @ return array < string , array < string , string >>
*/
protected function getUserDomPdfFontFamilies () : array
{
$fontStore = storage_path ( 'fonts/dompdf' );
if ( ! is_dir ( $fontStore )) {
return [];
}
$fontFamilies = [];
$fontFiles = glob ( $fontStore . DIRECTORY_SEPARATOR . '*.ttf' );
foreach ( $fontFiles as $fontFile ) {
$fontFileName = basename ( $fontFile , '.ttf' );
$expectedUfm = $fontStore . DIRECTORY_SEPARATOR . $fontFileName . '.ufm' ;
if ( ! file_exists ( $expectedUfm )) {
$font = Font :: load ( $fontFile );
$font -> parse ();
2026-04-22 13:22:20 +01:00
try {
$font -> saveAdobeFontMetrics ( $expectedUfm );
} catch ( \Exception $exception ) {
throw new PdfExportException ( " Failed to create required font data at $expectedUfm , Ensure this location is writable by the web server " );
}
2026-04-20 15:42:28 +01:00
}
$nameParts = explode ( '-' , $fontFileName );
if ( count ( $nameParts ) === 1 || $nameParts [ 1 ] === 'Regular' ) {
$nameParts [ 1 ] = 'Normal' ;
}
$family = trim ( strtolower ( preg_replace ( '/([A-Z])/' , ' $1' , $nameParts [ 0 ])));
$variation = Str :: snake ( $nameParts [ 1 ]);
if ( ! isset ( $fontFamilies [ $family ])) {
$fontFamilies [ $family ] = [];
}
$fontFamilies [ $family ][ $variation ] = $fontStore . DIRECTORY_SEPARATOR . $fontFileName ;
}
return $fontFamilies ;
}
2024-04-24 16:09:53 +01: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 15:39:40 +01:00
'{output_pdf_path}' => $outputPdf ,
2024-04-24 16:09:53 +01:00
];
foreach ( $replacementsByPlaceholder as $placeholder => $replacement ) {
$command = str_replace ( $placeholder , escapeshellarg ( $replacement ), $command );
}
file_put_contents ( $inputHtml , $html );
2024-09-27 16:33:58 +01:00
$timeout = intval ( config ( 'exports.pdf_command_timeout' ));
2024-04-24 16:09:53 +01:00
$process = Process :: fromShellCommandline ( $command );
2024-09-27 16:33:58 +01:00
$process -> setTimeout ( $timeout );
2025-01-01 15:19:11 +00:00
$cleanup = function () use ( $inputHtml , $outputPdf ) {
foreach ([ $inputHtml , $outputPdf ] as $file ) {
if ( file_exists ( $file )) {
unlink ( $file );
}
}
};
2024-09-27 16:33:58 +01:00
try {
$process -> run ();
} catch ( ProcessTimedOutException $e ) {
2025-01-01 15:19:11 +00:00
$cleanup ();
2024-09-27 16:33:58 +01:00
throw new PdfExportException ( " PDF Export via command failed due to timeout at { $timeout } second(s) " );
}
2024-04-24 16:09:53 +01:00
if ( ! $process -> isSuccessful ()) {
2025-01-01 15:19:11 +00:00
$cleanup ();
2024-04-24 16:09:53 +01:00
throw new PdfExportException ( " PDF Export via command failed with exit code { $process -> getExitCode () } , stdout: { $process -> getOutput () } , stderr: { $process -> getErrorOutput () } " );
}
$pdfContents = file_get_contents ( $outputPdf );
2025-01-01 15:19:11 +00:00
$cleanup ();
2024-04-24 16:09:53 +01:00
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 15:13:44 +01: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 16:40:42 +01: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-24 17:24:00 +00:00
}
2021-11-28 21:01:35 +00:00
}