diff --git a/app/Config/exports.php b/app/Config/exports.php index 2e22bc759e3..f48fe0a67a3 100644 --- a/app/Config/exports.php +++ b/app/Config/exports.php @@ -68,7 +68,7 @@ * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, * Symbol, ZapfDingbats. */ - 'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) + 'font_dir' => storage_path('fonts/dompdf'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) /** * The location of the DOMPDF font cache directory. @@ -78,7 +78,7 @@ * * Note: This directory must exist and be writable by the webserver process. */ - 'font_cache' => storage_path('fonts/'), + 'font_cache' => storage_path('fonts/dompdf/cache'), /** * The location of a temporary directory. diff --git a/app/Exports/PdfGenerator.php b/app/Exports/PdfGenerator.php index f31d8aad078..5506fe74065 100644 --- a/app/Exports/PdfGenerator.php +++ b/app/Exports/PdfGenerator.php @@ -4,6 +4,8 @@ use BookStack\Exceptions\PdfExportException; use Dompdf\Dompdf; +use FontLib\Font; +use Illuminate\Support\Str; use Knp\Snappy\Pdf as SnappyPdf; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; @@ -60,12 +62,65 @@ protected function renderUsingDomPdf(string $html): string $domPdf = new Dompdf($options); $domPdf->setBasePath(base_path('public')); + $fontMetrics = $domPdf->getFontMetrics(); + $userFontfamilies = $this->getUserDomPdfFontFamilies(); + foreach ($userFontfamilies as $fontFamily => $fonts) { + 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"); + } + } + $domPdf->loadHTML($this->convertEntities($html)); $domPdf->render(); return (string) $domPdf->output(); } + /** + * @return array> + */ + 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(); + 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"); + } + } + + $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; + } + /** * @throws PdfExportException */ diff --git a/storage/fonts/.gitignore b/storage/fonts/.gitignore index c96a04f008e..cb0b47dace2 100755 --- a/storage/fonts/.gitignore +++ b/storage/fonts/.gitignore @@ -1,2 +1,6 @@ +# Font cache files have once been stored directly in this folder +# therefore its important the contents non-ignored by git +# are chosen selectively * -!.gitignore \ No newline at end of file +!.gitignore +!dompdf/ \ No newline at end of file diff --git a/storage/fonts/dompdf/.gitignore b/storage/fonts/dompdf/.gitignore new file mode 100644 index 00000000000..23ef65311b4 --- /dev/null +++ b/storage/fonts/dompdf/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!cache/ \ No newline at end of file diff --git a/storage/fonts/dompdf/cache/.gitignore b/storage/fonts/dompdf/cache/.gitignore new file mode 100644 index 00000000000..c96a04f008e --- /dev/null +++ b/storage/fonts/dompdf/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/Exports/PdfExportTest.php b/tests/Exports/PdfExportTest.php index f311f8457db..78da3b0c2cc 100644 --- a/tests/Exports/PdfExportTest.php +++ b/tests/Exports/PdfExportTest.php @@ -79,6 +79,39 @@ public function test_page_pdf_export_opens_details_blocks() $this->assertStringContainsString('
entities->page()->forceFill([ + 'html' => '

Boldtext

', + ]); + $page->save(); + $this->setSettings([ + 'app-custom-head' => '' + ]); + $normalFont = $this->files->testFilePath('fonts/Cardiff.ttf'); + $normalFontTarget = storage_path('fonts/dompdf/MeowWords.ttf'); + $boldFont = $this->files->testFilePath('fonts/Cardiff-Bold.ttf'); + $boldFontTarget = storage_path('fonts/dompdf/MeowWords-Bold.ttf'); + copy($normalFont, $normalFontTarget); + copy($boldFont, $boldFontTarget); + + $resp = $this->asEditor()->get($page->getUrl('/export/pdf')); + $resp->assertStatus(200); + + // Existance of UFM files indicates the metrics have been generated + $this->assertFileExists(storage_path('fonts/dompdf/MeowWords.ufm')); + $this->assertFileExists(storage_path('fonts/dompdf/MeowWords-Bold.ufm')); + // Existence of cache json files indicates the fonts have been used + $this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords.ufm.json')); + $this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords-Bold.ufm.json')); + + $filesToCleanUp = [...glob(storage_path('fonts/dompdf/Meow*')), ...glob(storage_path('fonts/dompdf/cache/Meow*'))]; + foreach ($filesToCleanUp as $file) { + unlink($file); + } + } + public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() { $page = $this->entities->page(); diff --git a/tests/test-data/fonts/Cardiff-Bold.ttf b/tests/test-data/fonts/Cardiff-Bold.ttf new file mode 100644 index 00000000000..efaae4d9eb0 Binary files /dev/null and b/tests/test-data/fonts/Cardiff-Bold.ttf differ diff --git a/tests/test-data/fonts/Cardiff.ttf b/tests/test-data/fonts/Cardiff.ttf new file mode 100644 index 00000000000..4e8fea609c6 Binary files /dev/null and b/tests/test-data/fonts/Cardiff.ttf differ diff --git a/tests/test-data/fonts/attribution.txt b/tests/test-data/fonts/attribution.txt new file mode 100644 index 00000000000..e78c3892557 --- /dev/null +++ b/tests/test-data/fonts/attribution.txt @@ -0,0 +1,2 @@ +Font files by Roger White, in public domain. +https://web.archive.org/web/20110609213636/http://www.rogersfonts.org.uk/