diff --git a/DokuImageProcessorDecorator.php b/DokuImageProcessorDecorator.php deleted file mode 100644 index d5aea140..00000000 --- a/DokuImageProcessorDecorator.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ -class DokuPDF extends Mpdf -{ - /** - * DokuPDF constructor. - * - * @param string $pagesize - * @param string $orientation - * @param int $fontsize - * - * @throws MpdfException - * @throws Exception - */ - public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsize = 11, $docLang = 'en') - { - global $conf; - global $lang; - - if (!defined('_MPDF_TEMP_PATH')) { - define('_MPDF_TEMP_PATH', $conf['tmpdir'] . '/dwpdf/' . random_int(1, 1000) . '/'); - } - io_mkdir_p(_MPDF_TEMP_PATH); - - $format = $pagesize; - if ($orientation == 'landscape') { - $format .= '-L'; - } - - switch ($docLang) { - case 'zh': - case 'zh-tw': - case 'ja': - case 'ko': - $mode = '+aCJK'; - break; - default: - $mode = 'UTF-8-s'; - } - - parent::__construct([ - 'mode' => $mode, - 'format' => $format, - 'default_font_size' => $fontsize, - 'ImageProcessorClass' => DokuImageProcessorDecorator::class, - 'tempDir' => _MPDF_TEMP_PATH, //$conf['tmpdir'] . '/tmp/dwpdf' - 'SHYlang' => $docLang, - ]); - - $this->autoScriptToLang = true; - $this->baseScript = 1; - $this->autoVietnamese = true; - $this->autoArabic = true; - $this->autoLangToFont = true; - - $this->ignore_invalid_utf8 = true; - $this->tabSpaces = 4; - - // assumed that global language can be used, maybe Bookcreator needs more nuances? - $this->SetDirectionality($lang['direction']); - } - - /** - * Cleanup temp dir - */ - public function __destruct() - { - io_rmdir(_MPDF_TEMP_PATH, true); - } - - /** - * Decode all paths, since DokuWiki uses XHTML compliant URLs - * - * @param string $path - * @param string $basepath - */ - public function GetFullPath(&$path, $basepath = '') - { - $path = htmlspecialchars_decode($path); - parent::GetFullPath($path, $basepath); - } -} diff --git a/_test/ActionPagenameSortTest.php b/_test/ActionPagenameSortTest.php deleted file mode 100644 index f7188d6c..00000000 --- a/_test/ActionPagenameSortTest.php +++ /dev/null @@ -1,104 +0,0 @@ -assertLessThan(0, $action->cbPagenameSort(['id' => 'bar'], ['id' => 'bar:start'])); - $this->assertGreaterThan(0, $action->cbPagenameSort(['id' => 'bar:bar'], ['id' => 'bar:start'])); - } - - /** - * @return array - * @see testPageNameSort - */ - public function providerPageNameSort() - { - return [ - [ - 'start pages sorted', - [ - 'bar', - 'bar:start', - 'bar:alpha', - 'bar:bar', - ], - ], - [ - 'pages and subspaces mixed', - [ - 'alpha', - 'beta:foo', - 'gamma', - ], - ], - [ - 'full test', - [ - 'start', - '01_page', - '10_page', - 'bar', - 'bar:start', - 'bar:1_page', - 'bar:2_page', - 'bar:10_page', - 'bar:22_page', - 'bar:aa_page', - 'bar:aa_page:detail1', - 'bar:zz_page', - 'foo', - 'foo:start', - 'foo:01_page', - 'foo:10_page', - 'foo:foo', - 'foo:zz_page', - 'ns', - 'ns:01_page', - 'ns:10_page', - 'ns:ns', - 'ns:zz_page', - 'zz_page', - ], - ], - ]; - } - - /** - * @dataProvider providerPageNameSort - * @param string $comment - * @param array $expected - */ - public function testPagenameSort($comment, $expected) - { - // prepare the array as expected in the sort function - $prepared = []; - foreach ($expected as $line) { - $prepared[] = ['id' => $line]; - } - - // the input is random - $input = $prepared; - shuffle($input); - - // run sort - $action = new \action_plugin_dw2pdf(); - usort($input, [$action, 'cbPagenameSort']); - - $this->assertSame($prepared, $input); - } -} - diff --git a/_test/BookCreatorLiveSelectionCollectorTest.php b/_test/BookCreatorLiveSelectionCollectorTest.php new file mode 100644 index 00000000..6502ed35 --- /dev/null +++ b/_test/BookCreatorLiveSelectionCollectorTest.php @@ -0,0 +1,54 @@ +set('selection', json_encode([$pageA, $pageB, $pageC])); // page B has mixed case to test cleanID() + + $collector = new BookCreatorLiveSelectionCollector(new Config()); + $this->assertSame( + [$pageA, 'playground:bookcreator:child'], // page C should be skipped as it does not exist + $collector->getPages() + ); + } + + /** + * Invalid JSON payloads must bubble up as JsonException so the caller can handle them. + */ + public function testThrowsOnInvalidJson(): void + { + global $INPUT; + $INPUT->set('selection', '{invalid'); + + $this->expectException(\JsonException::class); + new BookCreatorLiveSelectionCollector(new Config()); + } +} diff --git a/_test/CollectorFactoryTest.php b/_test/CollectorFactoryTest.php new file mode 100644 index 00000000..56ea1589 --- /dev/null +++ b/_test/CollectorFactoryTest.php @@ -0,0 +1,90 @@ +assertInstanceOf(PageCollector::class, $collector); + } + + /** + * Factory should switch to namespace collector when export_pdfns is requested. + */ + public function testCreatesNamespaceCollector(): void + { + global $INPUT; + $INPUT->set('book_ns', 'wiki'); + $config = new Config(); + $collector = CollectorFactory::create('export_pdfns', $config, null, null); + $this->assertInstanceOf(NamespaceCollector::class, $collector); + } + + /** + * When a live selection payload is present, the live collector is used. + */ + public function testCreatesLiveSelectionCollector(): void + { + global $INPUT; + $INPUT->set('selection', '["wiki:start"]'); + $config = new Config(); + $collector = CollectorFactory::create('export_pdfbook', $config, null, null); + $this->assertInstanceOf(BookCreatorLiveSelectionCollector::class, $collector); + } + + /** + * When only a saved selection identifier is present, use the saved collector. + */ + public function testCreatesSavedSelectionCollector(): void + { + global $INPUT; + $INPUT->set('savedselection', 'my-saved-selection'); + $config = new Config(); + $collector = CollectorFactory::create('export_pdfbook', $config, null, null); + $this->assertInstanceOf(BookCreatorSavedSelectionCollector::class, $collector); + } + + /** + * A pdfbook export without any selection should be rejected. + */ + public function testBadSelectionCollector(): void + { + $this->expectException(\InvalidArgumentException::class); + $config = new Config(); + CollectorFactory::create('export_pdfbook', $config, null, null); + } + + /** + * Unknown export actions should be rejected early. + */ + public function testRejectsUnknownEvent(): void + { + $this->expectException(\InvalidArgumentException::class); + $config = new Config(); + CollectorFactory::create('random', $config, null, null); + } +} diff --git a/_test/ConfigTest.php b/_test/ConfigTest.php new file mode 100644 index 00000000..a0342713 --- /dev/null +++ b/_test/ConfigTest.php @@ -0,0 +1,194 @@ +getMPdfConfig(); + + $this->assertSame('A4', $config->getFormat(), 'default pagesize/orientation'); + $this->assertFalse($config->hasToc(), 'default toc'); + $this->assertSame(5, $config->getMaxBookmarks(), 'default maxbookmarks'); + $this->assertFalse($config->useNumberedHeaders(), 'default headernumber'); + $this->assertSame('', $config->getWatermarkText(), 'default watermark'); + $this->assertSame('default', $config->getTemplateName(), 'default template'); + $this->assertSame('file', $config->getOutputTarget(), 'default output'); + $this->assertSame([], $config->getStyledExtensions(), 'default usestyles'); + $this->assertSame(0.0, $config->getQRScale(), 'default qrcodescale'); + $this->assertFalse($config->isDebugEnabled(), 'default debug'); + $this->assertNull($config->getBookTitle(), 'default book_title'); + $this->assertSame('', $config->getBookNamespace(), 'default book_ns'); + $this->assertSame('natural', $config->getBookSortOrder(), 'default book_order'); + $this->assertSame(0, $config->getBookNamespaceDepth(), 'default book_nsdepth'); + $this->assertSame([], $config->getBookExcludedPages(), 'default excludes'); + $this->assertSame([], $config->getBookExcludedNamespaces(), 'default excludesns'); + $this->assertFalse($config->hasLiveSelection(), 'default selection flag'); + $this->assertNull($config->getLiveSelection(), 'default selection'); + $this->assertFalse($config->hasSavedSelection(), 'default savedselection flag'); + $this->assertNull($config->getSavedSelection(), 'default savedselection'); + $this->assertSame('', $config->getExportId(), 'default exportid'); + + $this->assertSame('A4', $mpdfConfig['format'], 'default pagesize/orientation'); + $this->assertSame(11, $mpdfConfig['default_font_size'], 'default font-size'); + $this->assertSame($conf['tmpdir'] . '/mpdf', $mpdfConfig['tempDir'], 'default tmpdir'); + $this->assertFalse($mpdfConfig['mirrorMargins'], 'default doublesided'); + $this->assertSame([], $mpdfConfig['h2toc'], 'default toc levels'); + $this->assertFalse($mpdfConfig['showWatermarkText'], 'default watermark'); + $this->assertSame('stretch', $mpdfConfig['setAutoTopMargin'], 'default mpdf auto top margin'); + $this->assertSame('stretch', $mpdfConfig['setAutoBottomMargin'], 'default mpdf auto bottom margin'); + $this->assertTrue($mpdfConfig['autoScriptToLang'], 'default mpdf autoScriptToLang'); + $this->assertSame(1, $mpdfConfig['baseScript'], 'default mpdf baseScript'); + $this->assertTrue($mpdfConfig['autoVietnamese'], 'default mpdf autoVietnamese'); + $this->assertTrue($mpdfConfig['autoArabic'], 'default mpdf autoArabic'); + $this->assertTrue($mpdfConfig['autoLangToFont'], 'default mpdf autoLangToFont'); + $this->assertTrue($mpdfConfig['ignore_invalid_utf8'], 'default mpdf ignore_invalid_utf8'); + $this->assertSame(4, $mpdfConfig['tabSpaces'], 'default mpdf tabSpaces'); + } + + /** + * Ensure overrides from config work as expected + */ + public function testloadPluginConfig(): void + { + $config = new Config([ + 'exportid' => 'playground:start', + 'pagesize' => 'Legal', + 'orientation' => 'landscape', + 'font-size' => 14, + 'doublesided' => 0, + 'toc' => 1, + 'toclevels' => '2-4', + 'maxbookmarks' => 3, + 'headernumber' => 1, + 'template' => 'modern', + 'output' => 'inline', + 'usestyles' => 'wrap,foo ', + 'watermark' => 'CONFIDENTIAL', + 'qrcodescale' => '2.5', + 'debug' => 1, + 'booktitle' => 'My Book', + 'booknamespace' => 'playground:sub', + 'booksortorder' => 'date', + 'booknamespacedepth' => 2, + 'bookexcludepages' => ['playground:sub:skip'], + 'bookexcludenamespaces' => ['playground:private'], + 'liveselection' => '["playground:start","playground:Sub:Child"]', + 'savedselection' => 'fav:123', + ]); + $mpdfConfig = $config->getMPdfConfig(); + + $this->assertSame('playground:start', $config->getExportId(), 'from exportid'); + $this->assertSame('Legal-L', $config->getFormat(), 'from pagesize + orientation'); + $this->assertSame(14, $mpdfConfig['default_font_size'], 'from font-size'); + $this->assertFalse($mpdfConfig['mirrorMargins'], 'from doublesided'); + $this->assertTrue($config->hasToc(), 'from toc'); + $this->assertSame(['H2' => 1, 'H3' => 2, 'H4' => 3], $mpdfConfig['h2toc'], 'from toclevels'); + $this->assertSame(3, $config->getMaxBookmarks(), 'from maxbookmarks'); + $this->assertTrue($config->useNumberedHeaders(), 'from headernumber'); + $this->assertSame('modern', $config->getTemplateName(), 'from template'); + $this->assertSame('inline', $config->getOutputTarget(), 'from output'); + $this->assertSame(['wrap', 'foo'], $config->getStyledExtensions(), 'from usestyles'); + $this->assertSame('CONFIDENTIAL', $config->getWatermarkText(), 'from watermark'); + $this->assertTrue($mpdfConfig['showWatermarkText'], 'from watermark'); + $this->assertSame(2.5, $config->getQRScale(), 'from qrcodescale'); + $this->assertTrue($config->isDebugEnabled(), 'from debug'); + $this->assertSame('My Book', $config->getBookTitle(), 'from booktitle'); + $this->assertSame('playground:sub', $config->getBookNamespace(), 'from booknamespace'); + $this->assertSame('date', $config->getBookSortOrder(), 'from booksortorder'); + $this->assertSame(2, $config->getBookNamespaceDepth(), 'from booknamespacedepth'); + $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages(), 'from bookexcludepages'); + $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces(), 'from bookexcludenamespaces'); + $this->assertTrue($config->hasLiveSelection(), 'from liveselection'); + $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection(), 'from liveselection'); + $this->assertTrue($config->hasSavedSelection(), 'from savedselection'); + $this->assertSame('fav:123', $config->getSavedSelection(), 'from savedselection'); + $this->assertNotEmpty($config->getCacheKey(), 'from combined plugin config values'); + } + + /** + * Ensure toc levels are set to DokuWiki's default when toc is enabled but no levels are set + */ + public function testDefaultTocLevels() + { + $config = new Config(['toc' => 1]); + $mpdfConfig = $config->getMPdfConfig(); + $this->assertSame(['H1' => 0, 'H2' => 1, 'H3' => 2], $mpdfConfig['h2toc'], 'from toclevels'); + } + + /** + * Ensure request parameters take precedence over defaults + */ + public function testloadInputConfig(): void + { + global $INPUT, $ID; + $ID = 'playground:start'; + $INPUT->set('pagesize', 'Legal'); + $INPUT->set('orientation', 'landscape'); + $INPUT->set('font-size', '14'); + $INPUT->set('doublesided', '0'); + $INPUT->set('toc', '1'); + $INPUT->set('toclevels', '2-4'); + $INPUT->set('watermark', 'CONFIDENTIAL'); + $INPUT->set('tpl', 'modern'); + $INPUT->set('debug', '1'); + $INPUT->set('outputTarget', 'inline'); + $INPUT->set('book_title', 'My Book'); + $INPUT->set('book_ns', 'playground:sub'); + $INPUT->set('book_order', 'date'); + $INPUT->set('book_nsdepth', 2); + $INPUT->set('excludes', ['playground:sub:skip']); + $INPUT->set('excludesns', ['playground:private']); + $INPUT->set('selection', '["playground:start","playground:Sub:Child"]'); + $INPUT->set('savedselection', 'fav:123'); + + + $config = new Config(); + $mpdfConfig = $config->getMPdfConfig(); + + $this->assertSame('playground:start', $config->getExportId(), 'from $ID'); + $this->assertSame('Legal-L', $config->getFormat(), 'from pagesize + orientation'); + $this->assertSame(14, $mpdfConfig['default_font_size'], 'from font-size'); + $this->assertFalse($mpdfConfig['mirrorMargins'], 'from doublesided'); + $this->assertSame(['H2' => 1, 'H3' => 2, 'H4' => 3], $mpdfConfig['h2toc'], 'from toclevels'); + $this->assertTrue($mpdfConfig['showWatermarkText'], 'from watermark'); + $this->assertSame('CONFIDENTIAL', $config->getWatermarkText(), 'from watermark'); + $this->assertSame('modern', $config->getTemplateName(), 'from tpl'); + $this->assertTrue($config->isDebugEnabled(), 'from debug'); + $this->assertSame('inline', $config->getOutputTarget(), 'from outputTarget'); + $this->assertSame('My Book', $config->getBookTitle(), 'from book_title'); + $this->assertSame('playground:sub', $config->getBookNamespace(), 'from book_ns'); + $this->assertSame('date', $config->getBookSortOrder(), 'from book_order'); + $this->assertSame(2, $config->getBookNamespaceDepth(), 'from book_nsdepth'); + $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages(), 'from excludes'); + $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces(), 'from excludesns'); + $this->assertTrue($config->hasLiveSelection(), 'from selection'); + $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection(), 'from selection'); + $this->assertTrue($config->hasSavedSelection(), 'from savedselection'); + $this->assertSame('fav:123', $config->getSavedSelection(), 'from savedselection'); + + } + + +} diff --git a/_test/DokuImageProcessorTest.php b/_test/DokuImageProcessorTest.php deleted file mode 100644 index 556cadf9..00000000 --- a/_test/DokuImageProcessorTest.php +++ /dev/null @@ -1,73 +0,0 @@ -assertEquals($expected_file, $actual_file, '$file ' . $msg); - $this->assertEquals($expected_orig_srcpath, $actual_orig_srcpath, '$orig_srcpath ' . $msg); - } - -} diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php new file mode 100644 index 00000000..0fbf9317 --- /dev/null +++ b/_test/EndToEndTest.php @@ -0,0 +1,292 @@ +prepareFixturePage($page); + } + + $config = new Config(array_merge( + $conf, + [ + 'debug' => 1, + 'liveselection' => json_encode($pages) + ] + )); + $collector = new BookCreatorLiveSelectionCollector($config); + $cache = new Cache($config, $collector); + $service = new PdfExportService($config, $collector, $cache, 'Contents', 'tester'); + return $service->getDebugHtml(); + } + + /** + * Test that numbered headers are rendered correctly + */ + public function testNumberedHeaders(): void + { + $html = $this->getDebugHTML('headers', ['headernumber' => 1]); + + $dom = (new Document())->html($html); + + $dom->find('h1')->each(function ($h) { + $this->assertMatchesRegularExpression('/^1\. Header/', $h->text()); + }); + + $dom->find('h2')->each(function ($h) { + $this->assertMatchesRegularExpression('/^\d\.\d\. Header/', $h->text()); + }); + + $dom->find('h3')->each(function ($h) { + $this->assertMatchesRegularExpression('/^\d\.\d\.\d\. Header/', $h->text()); + }); + } + + /** + * Test that numbered headers are rendered correctly across multiple pages + * + * Each new page should increase the top-level header number + */ + public function testNumberedHeadersMultipage(): void + { + $html = $this->getDebugHTML(['headers', 'simple'], ['headernumber' => 1]); + + $dom = (new Document())->html($html); + + $count = 1; + $dom->find('h1')->each(function ($h) use (&$count) { + $this->assertMatchesRegularExpression('/^' . ($count++) . '\. /', $h->text()); + }); + } + + /** + * Ensure each rendered page begins with an anchor that namespaces intra-page links. + */ + public function testDocumentStartCreatesPageAnchors(): void + { + $html = $this->getDebugHTML(['renderer_features', 'target']); + + $dom = (new Document())->html($html); + + foreach (['renderer_features', 'target'] as $pageId) { + $anchors = $dom->find('a[name="' . $pageId . '__"]'); + $this->assertSame( + 1, + count($anchors), + 'Missing document_start anchor for ' . $pageId + ); + } + } + + /** + * Bookmarks should only be produced up to the configured level and include numbering. + */ + public function testBookmarksRespectConfiguredLevels(): void + { + $html = $this->getDebugHTML('headers', ['headernumber' => 1, 'maxbookmarks' => 2]); + + $dom = (new Document())->html($html); + $bookmarks = $dom->find('bookmark'); + + $this->assertGreaterThan(0, count($bookmarks)); + + foreach ($bookmarks as $bookmark) { + $this->assertLessThanOrEqual( + 1, + (int)$bookmark->attr('level'), + 'Bookmark level exceeded configured maximum' + ); + + $content = trim((string)$bookmark->attr('content')); + $this->assertMatchesRegularExpression('/^\d+(?:\.\d+)*\.\s+Header/', $content); + } + + $this->assertSame(count($dom->find('h1, h2')), count($bookmarks)); + } + + /** + * Local section links should include the page-specific prefix. + */ + public function testLocallinksArePrefixedWithPageId(): void + { + $html = $this->getDebugHTML(['renderer_features', 'target']); + $dom = (new Document())->html($html); + + $link = $this->findLinkByText($dom, 'Jump to Remote Section'); + $this->assertNotNull($link, 'Local section link missing'); + + $this->assertSame('#renderer_features__remote_section', $link->attr('href')); + } + + /** + * Internal links must expose dw2pdf data attributes so the writer can retarget them. + */ + public function testInternalLinksExposeDw2pdfMetadata(): void + { + $html = $this->getRawRendererHtml('renderer_features', [], ['target']); + $dom = (new Document())->html($html); + + $pageLink = $this->findLinkByText($dom, 'Target page link'); + $this->assertNotNull($pageLink, 'Page link missing'); + $this->assertSame('target', $pageLink->attr('data-dw2pdf-target')); + $this->assertSame('', $pageLink->attr('data-dw2pdf-hash')); + + $sectionLink = $this->findLinkByText($dom, 'Target section link'); + $this->assertNotNull($sectionLink, 'Section link missing'); + $this->assertSame('target', $sectionLink->attr('data-dw2pdf-target')); + $this->assertSame('sub_section', $sectionLink->attr('data-dw2pdf-hash')); + } + + /** + * Centered media needs to be wrapped so CSS centering survives inside mPDF. + */ + public function testCenteredMediaIsWrapped(): void + { + $html = $this->getDebugHTML('renderer_features'); + $dom = (new Document())->html($html); + + $wrappers = $dom->find('div[align="center"][style*="text-align: center"]'); + $this->assertGreaterThan(0, count($wrappers), 'Centered media wrapper missing'); + $this->assertGreaterThan(0, count($wrappers->first()->find('img'))); + } + + /** + * Acronyms should render as plain text to avoid useless hover hints in PDFs. + */ + public function testAcronymOutputDropsHover(): void + { + $html = $this->getDebugHTML('renderer_features'); + $dom = (new Document())->html($html); + + $this->assertSame(0, count($dom->find('acronym'))); + $this->assertStringContainsString('FAQ', $html); + $this->assertStringNotContainsString('Frequently Asked Questions', $html); + } + + /** + * Email addresses must not be obfuscated so that mailto links remain readable. + */ + public function testEmailLinksStayReadable(): void + { + $html = $this->getDebugHTML('renderer_features'); + $dom = (new Document())->html($html); + + $link = $this->findLinkByText($dom, 'test@example.com'); + $this->assertNotNull($link, 'Email link missing'); + $this->assertSame('mailto:test@example.com', $link->attr('href')); + } + + /** + * Interwiki links should be prefixed with the respective icon. + */ + public function testInterwikiLinksArePrefixedWithIcon(): void + { + $html = $this->getDebugHTML('renderer_features'); + $dom = (new Document())->html($html); + + $link = $dom->find('a.interwiki')->first(); + $this->assertNotNull($link, 'Interwiki link missing'); + + $icon = $link->children()->first(); + $this->assertNotNull($icon, 'Interwiki icon missing'); + $this->assertSame('img', strtolower($icon->nodeName)); + $this->assertStringContainsString('iw_doku', $icon->attr('class')); + } + + /** + * Render a single page through the dw2pdf renderer without writer post-processing. + * + * @param string $pageId + * @param array $conf + * @param string[] $additionalPages + * @return string + */ + protected function getRawRendererHtml(string $pageId, array $conf = [], array $additionalPages = []): string + { + $this->prepareFixturePage($pageId); + foreach ($additionalPages as $related) { + $this->prepareFixturePage($related); + } + + /** @var \renderer_plugin_dw2pdf $renderer */ + $renderer = plugin_load('renderer', 'dw2pdf', true); + $renderer->setConfig(new Config($conf)); + + global $ID; + $keep = $ID; + $ID = $pageId; + + $file = wikiFN($pageId); + $instructions = p_get_instructions(io_readWikiPage($file, $pageId)); + $info = []; + $html = p_render('dw2pdf', $instructions, $info); + + $ID = $keep; + + return $html; + } + + /** + * Persist the given fixture page into the wiki so that it can be rendered. + * + * @param string $pageId + * @return void + */ + protected function prepareFixturePage(string $pageId): void + { + $data = file_get_contents(__DIR__ . '/pages/' . $pageId . '.txt'); + saveWikiText($pageId, $data, 'dw2pdf renderer test'); + } + + /** + * Locate the first hyperlink whose trimmed text matches the expected label. + * + * @param Document $dom + * @param string $text + * @return Element|null + */ + protected function findLinkByText(Document $dom, string $text): ?Element + { + foreach ($dom->find('a') as $anchor) { + if (trim($anchor->text()) === $text) { + return $anchor; + } + } + + return null; + } +} diff --git a/_test/GeneralTest.php b/_test/GeneralTest.php index 8c6d5b07..a228afcc 100644 --- a/_test/GeneralTest.php +++ b/_test/GeneralTest.php @@ -32,9 +32,9 @@ public function testPluginInfo(): void $this->assertArrayHasKey('url', $info); $this->assertEquals('dw2pdf', $info['base']); - $this->assertRegExp('/^https?:\/\//', $info['url']); + $this->assertMatchesRegularExpression('/^https?:\/\//', $info['url']); $this->assertTrue(mail_isvalid($info['email'])); - $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']); + $this->assertMatchesRegularExpression('/^\d\d\d\d-\d\d-\d\d$/', $info['date']); $this->assertTrue(false !== strtotime($info['date'])); } diff --git a/_test/LocalContentLoaderTest.php b/_test/LocalContentLoaderTest.php new file mode 100644 index 00000000..9251e224 --- /dev/null +++ b/_test/LocalContentLoaderTest.php @@ -0,0 +1,47 @@ +markTestSkipped('Unable to create a temporary file'); + } + + $file = $temp . '.png'; + if (!@rename($temp, $file)) { + $this->fail('Unable to prepare a temporary dw2pdf:// file'); + } + file_put_contents($file, 'image-bytes'); + + $loader = new LocalContentLoader(); + $this->assertSame('image-bytes', $loader->load('dw2pdf://' . $file)); + + @unlink($file); + } + + /** + * Missing files should return null so mPDF can fall back to HTTP. + */ + public function testReturnsNullForMissingFiles(): void + { + $file = sys_get_temp_dir() . '/dw2pdf_loader_missing.png'; + @unlink($file); + + $loader = new LocalContentLoader(); + $this->assertNull($loader->load($file)); + } +} diff --git a/_test/MediaLinkResolverTest.php b/_test/MediaLinkResolverTest.php new file mode 100644 index 00000000..7f8ce123 --- /dev/null +++ b/_test/MediaLinkResolverTest.php @@ -0,0 +1,130 @@ +resolver = new MediaLinkResolver(); + } + + /** + * @return array + */ + public static function resolveProvider(): array + { + global $conf; + + return [ + 'internal fetch url' => [ + DOKU_URL . 'lib/exe/fetch.php?media=wiki:dokuwiki-128.png', + $conf['mediadir'] . '/wiki/dokuwiki-128.png', + 'image/png', + ], + 'static local file' => [ + DOKU_URL . 'lib/images/throbber.gif', + DOKU_INC . 'lib/images/throbber.gif', + 'image/gif', + ], + ]; + } + + /** + * @dataProvider resolveProvider + */ + public function testResolveReturnsLocalPathAndMime(string $input, string $expectedPath, string $expectedMime): void + { + $resolved = $this->resolver->resolve($input); + + $this->assertNotNull($resolved); + $this->assertSame($expectedPath, $resolved['path']); + $this->assertSame($expectedMime, $resolved['mime']); + $this->assertFileExists($resolved['path']); + } + + /** + * The resolver must support our dw2pdf:// pseudo scheme for temporary files. + */ + public function testResolveDw2pdfScheme(): void + { + $temp = tempnam(sys_get_temp_dir(), 'dw2pdf'); + if ($temp === false) { + $this->fail('Unable to create temp file for dw2pdf:// test'); + } + + $image = $temp . '.png'; + if (!rename($temp, $image)) { + $this->fail('Unable to rename temp file for dw2pdf:// test'); + } + + $resolved = $this->resolver->resolve('dw2pdf://' . $image); + + $this->assertNotNull($resolved); + $this->assertSame($image, $resolved['path']); + $this->assertSame('image/png', $resolved['mime']); + + @unlink($image); + } + + /** + * @group internet + */ + public function testResolveFetchesExternalMedia(): void + { + global $conf; + $conf['fetchsize'] = 512 * 1024; // 512 KB + + $external = 'https://php.net/images/php.gif'; + $input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external); + $resolved = $this->resolver->resolve($input); + + if ($resolved === null) { + $this->markTestSkipped('External media fetching is not available in this environment.'); + } + + $this->assertNotNull($resolved); + $this->assertFileExists($resolved['path']); + $this->assertSame('image/gif', $resolved['mime']); + $this->assertSame(2523, filesize($resolved['path'])); + } + + /** + * Non-image payloads should never be returned to the PDF generator. + */ + public function testResolveRejectsNonImages(): void + { + $resolved = $this->resolver->resolve(DOKU_URL . 'README'); + + $this->assertNull($resolved); + } + + /** + * Resizing parameters from fetch.php should trigger scaled copies in cache. + */ + public function testResolveAppliesResizeParameter(): void + { + global $conf; + + $input = DOKU_URL . 'lib/exe/fetch.php?w=32&media=wiki:dokuwiki-128.png'; + $original = $conf['mediadir'] . '/wiki/dokuwiki-128.png'; + $resolved = $this->resolver->resolve($input); + + $this->assertNotNull($resolved); + $this->assertNotSame($original, $resolved['path']); + $this->assertSame('image/png', $resolved['mime']); + $this->assertFileExists($resolved['path']); + } +} diff --git a/_test/NamespaceCollectorCollectTest.php b/_test/NamespaceCollectorCollectTest.php new file mode 100644 index 00000000..37e7f75f --- /dev/null +++ b/_test/NamespaceCollectorCollectTest.php @@ -0,0 +1,62 @@ +createPage("$ns:start", 'start page'); + $this->createPage("$ns:keep", 'keep me'); + $this->createPage("$ns:skip", 'skip me'); + $this->createPage("$ns:child:page", 'child namespace'); + + $INPUT->set('book_ns', $ns); + $INPUT->set('book_order', 'pagename'); + $INPUT->set('excludes', ["$ns:skip"]); + $INPUT->set('excludesns', ["$ns:child"]); + + $collector = new NamespaceCollector(new Config()); + $this->assertSame( + ["$ns:start", "$ns:keep"], + $collector->getPages() + ); + } + + /** + * Invalid namespaces must be ignored gracefully. + */ + public function testCollectReturnsEmptyForMissingNamespace(): void + { + global $INPUT; + $INPUT->set('book_ns', 'missing:dw2pdfns'); + + $collector = new NamespaceCollector(new Config()); + $this->assertSame([], $collector->getPages()); + } + + private function createPage(string $id, string $content): void + { + saveWikiText($id, $content, 'dw2pdf namespace test'); + } +} diff --git a/_test/NamespaceCollectorSortTest.php b/_test/NamespaceCollectorSortTest.php new file mode 100644 index 00000000..b9b207ac --- /dev/null +++ b/_test/NamespaceCollectorSortTest.php @@ -0,0 +1,91 @@ + [[ + 'bar', + 'bar:start', + 'bar:alpha', + 'bar:bar', + ]], + + 'pages and subspaces mixed' => [[ + 'alpha', + 'beta:foo', + 'gamma', + ]], + + 'full test' => [[ + 'start', + '01_page', + '10_page', + 'bar', + 'bar:start', + 'bar:1_page', + 'bar:2_page', + 'bar:10_page', + 'bar:22_page', + 'bar:aa_page', + 'bar:aa_page:detail1', + 'bar:zz_page', + 'foo', + 'foo:start', + 'foo:01_page', + 'foo:10_page', + 'foo:foo', + 'foo:zz_page', + 'ns', + 'ns:01_page', + 'ns:10_page', + 'ns:ns', + 'ns:zz_page', + 'zz_page', + ]], + ]; + } + + /** + * Ensure natural name sorting remains stable for multiple namespace scenarios. + * + * @dataProvider providerPageNameSort + * @param array $expected + */ + public function testPagenameSort(array $expected): void + { + // Build a namespace collector instance without running the heavy constructor logic. + $reflection = new ReflectionClass(NamespaceCollector::class); + /** @var NamespaceCollector $collector */ + $collector = $reflection->newInstanceWithoutConstructor(); + + $prepared = []; + foreach ($expected as $line) { + $prepared[] = ['id' => $line]; + } + + $input = $prepared; + shuffle($input); + + usort($input, [$collector, 'cbPagenameSort']); + + $this->assertSame($prepared, $input); + } +} diff --git a/_test/PageCollectorTest.php b/_test/PageCollectorTest.php new file mode 100644 index 00000000..748ebab1 --- /dev/null +++ b/_test/PageCollectorTest.php @@ -0,0 +1,49 @@ +set('id', $ID); + saveWikiText($ID, 'DW2PDF page content', 'create test page'); + + $collector = new PageCollector(new Config()); + $this->assertSame([$ID], $collector->getPages()); + } + + /** + * Missing pages should not be exported. + */ + public function testReturnsEmptyForMissingPage(): void + { + global $ID, $INPUT; + $ID = 'playground:missingdw2pdf'; + $INPUT->set('id', $ID); + + @unlink(wikiFN($ID)); + + $collector = new PageCollector(new Config()); + $this->assertSame([], $collector->getPages()); + } +} diff --git a/_test/TemplateTest.php b/_test/TemplateTest.php new file mode 100644 index 00000000..737edb90 --- /dev/null +++ b/_test/TemplateTest.php @@ -0,0 +1,65 @@ +set('book_title', 'Export Title'); + $ID = 'playground:templatepage'; + saveWikiText($ID, 'template test', 'create'); + + $config = new Config([ + 'qrcodescale' => 1.5, + ]); + + $collector = new PageCollector($config); + $template = new Template($config); + $template->setContext($collector, $ID, 'username'); + + $html = $template->getHTML('unittest'); + + $this->assertStringContainsString('Page Number: {PAGENO}', $html); + $this->assertStringContainsString('Total Pages: {nbpg}', $html); + $this->assertStringContainsString('Document Title: Export Title', $html); + $this->assertStringContainsString('Wiki Title: ' . $conf['title'], $html); + $this->assertStringContainsString('Wiki URL: ' . DOKU_URL, $html); + $this->assertStringNotContainsString('@DATE@', $html); + $this->assertStringContainsString('User: username', $html); + $this->assertStringContainsString('Base Path: ' . DOKU_BASE, $html); + $this->assertStringContainsString('Include Dir: ' . DOKU_INC, $html); + $this->assertStringContainsString('Template Base Path: ' . DOKU_BASE . 'lib/plugins/dw2pdf/tpl/default/', $html); + $this->assertStringContainsString('Template Include Dir: ' . DOKU_INC . 'lib/plugins/dw2pdf/tpl/default/', $html); + $this->assertStringContainsString('Page ID: ' . $ID, $html); + + $revisionDate = dformat(filemtime(wikiFN($ID))); + $this->assertStringContainsString('Revision: ' . $revisionDate, $html); + + $pageUrl = wl($ID, [], true, '&'); + $this->assertStringContainsString('Page URL: ' . $pageUrl, $html); + $this->assertStringContainsString('assertStringContainsString('size="1.5"', $html); + $this->assertStringNotContainsString('@QRCODE@', $html); + } +} diff --git a/_test/WriterInternalLinksTest.php b/_test/WriterInternalLinksTest.php new file mode 100644 index 00000000..3c1c1161 --- /dev/null +++ b/_test/WriterInternalLinksTest.php @@ -0,0 +1,71 @@ +' + . 'First' + // missing:id must keep its original href because the collector will not output that page + . 'Second' + . '

'; + + $writer = (new \ReflectionClass(Writer::class))->newInstanceWithoutConstructor(); + $method = (new \ReflectionClass(Writer::class))->getMethod('fixInternalLinks'); + $method->setAccessible(true); + $result = $method->invoke($writer, $collector, $html); + + $check = false; + $section = sectionID('playground:fixlinks', $check); + $this->assertStringContainsString('href="#' . $section . '__abc"', $result); + $this->assertStringNotContainsString('data-dw2pdf-target', $result); + $this->assertStringContainsString('href="doku.php?id=missing"', $result); + } +} + +class WriterInternalLinksCollectorStub extends AbstractCollector +{ + /** @var string[] */ + private array $collectedPages; + + public function __construct(array $pages, Config $config) + { + $this->collectedPages = $pages; + $this->config = $config; + $this->pages = $pages; + $this->title = 'stub'; + $this->rev = null; + $this->at = null; + } + + protected function collect(): array + { + return $this->collectedPages; + } +} diff --git a/_test/pages/headers.txt b/_test/pages/headers.txt new file mode 100644 index 00000000..5228aa95 --- /dev/null +++ b/_test/pages/headers.txt @@ -0,0 +1,17 @@ +====== Header A ====== + +===== Header A.A ===== + +==== Header A.A.a ===== + +=== Header A.A.a.a === + +=== Header A.A.a.b === + +==== Header A.A.b ===== + +==== Header A.A.c ===== + +===== Header A.B ====== + +===== Header A.C ====== diff --git a/_test/pages/renderer_features.txt b/_test/pages/renderer_features.txt new file mode 100644 index 00000000..79c2c515 --- /dev/null +++ b/_test/pages/renderer_features.txt @@ -0,0 +1,29 @@ +====== Renderer Feature Playground ====== + +===== Local Navigation ===== + +[[#remote_section|Jump to Remote Section]] + +===== Internal Links ===== + +[[target|Target page link]] and [[target#sub_section|Target section link]] + +===== Media ===== + +{{ wiki:dokuwiki-128.png?50 }} + +===== Acronym ===== + +This is an example of an acronym: FAQ. + +===== Email ===== + +Contact for mission details. + +===== Interwiki ===== + +See [[doku>syntax|DokuWiki Syntax]] for more. + +===== Remote Section ===== + +This section anchors local jump links. diff --git a/_test/pages/simple.txt b/_test/pages/simple.txt new file mode 100644 index 00000000..fdbb21b2 --- /dev/null +++ b/_test/pages/simple.txt @@ -0,0 +1,3 @@ +====== A Simple Page ====== + +This is a simple page with simple text for simple testing. diff --git a/_test/pages/target.txt b/_test/pages/target.txt new file mode 100644 index 00000000..ab9ee4f5 --- /dev/null +++ b/_test/pages/target.txt @@ -0,0 +1,5 @@ +====== Target Page ====== + +===== Sub Section ===== + +This page provides an anchor for cross-page links. diff --git a/action.php b/action.php index 1b665733..5ad6bf0e 100644 --- a/action.php +++ b/action.php @@ -1,11 +1,13 @@ */ - protected $tpl; - /** @var string title of exported pdf */ - protected $title; - /** @var array list of pages included in exported pdf */ - protected $list = []; - /** @var bool|string path to temporary cachefile */ - protected $onetimefile = false; - protected $currentBookChapter = 0; - - /** - * Constructor. Sets the correct template - */ - public function __construct() - { - require_once __DIR__ . '/vendor/autoload.php'; - - $this->tpl = $this->getExportConfig('template'); - } - - /** - * Delete cached files that were for one-time use - */ - public function __destruct() - { - if ($this->onetimefile) { - unlink($this->onetimefile); - } - } - - /** - * Return the value of currentBookChapter, which is the order of the file to be added in a book generation - */ - public function getCurrentBookChapter() - { - return $this->currentBookChapter; - } - /** * Register the events * @@ -72,7 +30,6 @@ public function getCurrentBookChapter() public function register(EventHandler $controller) { $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert'); - $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton'); $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addsvgbutton'); } @@ -80,1003 +37,38 @@ public function register(EventHandler $controller) * Do the HTML to PDF conversion work * * @param Event $event + * @throws MpdfException */ public function convert(Event $event) { - global $REV, $DATE_AT; - global $conf, $INPUT; + global $REV, $DATE_AT, $INPUT; // our event? $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; if (!in_array($event->data, $allowedEvents)) { return; } - - try { - //collect pages and check permissions - [$this->title, $this->list] = $this->collectExportablePages($event); - - if ($event->data === 'export_pdf' && ($REV || $DATE_AT)) { - $cachefile = tempnam($conf['tmpdir'] . '/dwpdf', 'dw2pdf_'); - $this->onetimefile = $cachefile; - $generateNewPdf = true; - } else { - // prepare cache and its dependencies - $depends = []; - $cache = $this->prepareCache($depends); - $cachefile = $cache->cache; - $generateNewPdf = !$this->getConf('usecache') - || $this->getExportConfig('isDebug') - || !$cache->useCache($depends); - } - - // hard work only when no cache available or needed for debugging - if ($generateNewPdf) { - // generating the pdf may take a long time for larger wikis / namespaces with many pages - set_time_limit(0); - //may throw Mpdf\MpdfException as well - $this->generatePDF($cachefile, $event); - } - } catch (Exception $e) { - if ($INPUT->has('selection')) { - http_status(400); - echo $e->getMessage(); - exit(); - } else { - //prevent Action/Export() - msg($e->getMessage(), -1); - $event->data = 'redirect'; - return; - } - } - $event->preventDefault(); // after prevent, $event->data cannot be changed - - // deliver the file - $this->sendPDFFile($cachefile); //exits - } - - /** - * Obtain list of pages and title, for different methods of exporting the pdf. - * - Return a title and selection, throw otherwise an exception - * - Check permisions - * - * @param Event $event - * @return array - * @throws Exception - */ - protected function collectExportablePages(Event $event) - { - global $ID, $REV; - global $INPUT; - global $conf, $lang; - - // list of one or multiple pages - $list = []; - - if ($event->data == 'export_pdf') { - if (auth_quickaclcheck($ID) < AUTH_READ) { // set more specific denied message - throw new Exception($lang['accessdenied']); - } - $list[0] = $ID; - $title = $INPUT->str('pdftitle'); //DEPRECATED - $title = $INPUT->str('book_title', $title, true); - if (empty($title)) { - $title = p_get_first_heading($ID); - } - // use page name if title is still empty - if (empty($title)) { - $title = noNS($ID); - } - - $filename = wikiFN($ID, $REV); - if (!file_exists($filename)) { - throw new Exception($this->getLang('notexist')); - } - } elseif ($event->data == 'export_pdfns') { - //check input for title and ns - if (!$title = $INPUT->str('book_title')) { - throw new Exception($this->getLang('needtitle')); - } - $pdfnamespace = cleanID($INPUT->str('book_ns')); - if (!@is_dir(dirname(wikiFN($pdfnamespace . ':dummy')))) { - throw new Exception($this->getLang('needns')); - } - - //sort order - $order = $INPUT->str('book_order', 'natural', true); - $sortoptions = ['pagename', 'date', 'natural']; - if (!in_array($order, $sortoptions)) { - $order = 'natural'; - } - - //search depth - $depth = $INPUT->int('book_nsdepth', 0); - if ($depth < 0) { - $depth = 0; - } - - //page search - $result = []; - $opts = ['depth' => $depth]; //recursive all levels - $dir = utf8_encodeFN(str_replace(':', '/', $pdfnamespace)); - search($result, $conf['datadir'], 'search_allpages', $opts, $dir); - - // exclude ids - $excludes = $INPUT->arr('excludes'); - if (!empty($excludes)) { - $result = array_filter($result, function ($item) use ($excludes) { - return !in_array($item['id'], $excludes); - }); - } - // exclude namespaces - $excludesns = $INPUT->arr('excludesns'); - if (!empty($excludesns)) { - $result = array_filter($result, function ($item) use ($excludesns) { - foreach ($excludesns as $ns) { - if (strpos($item['id'], $ns . ':') === 0) { - return false; - } - } - return true; - }); - } - - //sorting - if (count($result) > 0) { - if ($order == 'date') { - usort($result, [$this, 'cbDateSort']); - } elseif ($order == 'pagename' || $order == 'natural') { - usort($result, [$this, 'cbPagenameSort']); - } - } - - foreach ($result as $item) { - $list[] = $item['id']; - } - - if ($pdfnamespace !== '') { - if (!in_array($pdfnamespace . ':' . $conf['start'], $list, true)) { - if (file_exists(wikiFN(rtrim($pdfnamespace, ':')))) { - array_unshift($list, rtrim($pdfnamespace, ':')); - } - } - } - } elseif (!empty($_COOKIE['list-pagelist'])) { - /** @deprecated April 2016 replaced by localStorage version of Bookcreator */ - //is in Bookmanager of bookcreator plugin a title given? - $title = $INPUT->str('pdfbook_title'); //DEPRECATED - $title = $INPUT->str('book_title', $title, true); - if (empty($title)) { - throw new Exception($this->getLang('needtitle')); - } - - $list = explode("|", $_COOKIE['list-pagelist']); - } elseif ($INPUT->has('selection')) { - //handle Bookcreator requests based at localStorage -// if(!checkSecurityToken()) { -// http_status(403); -// print $this->getLang('empty'); -// exit(); -// } - - $list = json_decode($INPUT->str('selection', '', true), true); - if (!is_array($list) || $list === []) { - throw new Exception($this->getLang('empty')); - } - - $title = $INPUT->str('pdfbook_title'); //DEPRECATED - $title = $INPUT->str('book_title', $title, true); - if (empty($title)) { - throw new Exception($this->getLang('needtitle')); - } - } elseif ($INPUT->has('savedselection')) { - //export a saved selection of the Bookcreator Plugin - if (plugin_isdisabled('bookcreator')) { - throw new Exception($this->getLang('missingbookcreator')); - } - /** @var action_plugin_bookcreator_handleselection $SelectionHandling */ - $SelectionHandling = plugin_load('action', 'bookcreator_handleselection'); - $savedselection = $SelectionHandling->loadSavedSelection($INPUT->str('savedselection')); - $title = $savedselection['title']; - $title = $INPUT->str('book_title', $title, true); - $list = $savedselection['selection']; - - if (empty($title)) { - throw new Exception($this->getLang('needtitle')); - } - } else { - //show empty bookcreator message - throw new Exception($this->getLang('empty')); - } - - $list = array_map('cleanID', $list); - - $skippedpages = []; - foreach ($list as $index => $pageid) { - if (auth_quickaclcheck($pageid) < AUTH_READ) { - $skippedpages[] = $pageid; - unset($list[$index]); - } - } - $list = array_filter($list, 'strlen'); //use of strlen() callback prevents removal of pagename '0' - - //if selection contains forbidden pages throw (overridable) warning - if (!$INPUT->bool('book_skipforbiddenpages') && $skippedpages !== []) { - $msg = hsc(implode(', ', $skippedpages)); - throw new Exception(sprintf($this->getLang('forbidden'), $msg)); - } - - return [$title, $list]; - } - - /** - * Prepare cache - * - * @param array $depends (reference) array with dependencies - * @return cache - */ - protected function prepareCache(&$depends) - { - global $REV; - - $cachekey = implode(',', $this->list) - . $REV - . $this->getExportConfig('template') - . $this->getExportConfig('pagesize') - . $this->getExportConfig('orientation') - . $this->getExportConfig('font-size') - . $this->getExportConfig('doublesided') - . $this->getExportConfig('headernumber') - . ($this->getExportConfig('hasToC') ? implode('-', $this->getExportConfig('levels')) : '0') - . $this->title; - $cache = new Cache($cachekey, '.dw2.pdf'); - - $dependencies = []; - foreach ($this->list as $pageid) { - $relations = p_get_metadata($pageid, 'relation'); - - if (is_array($relations)) { - if (array_key_exists('media', $relations) && is_array($relations['media'])) { - foreach ($relations['media'] as $mediaid => $exists) { - if ($exists) { - $dependencies[] = mediaFN($mediaid); - } - } - } - - if (array_key_exists('haspart', $relations) && is_array($relations['haspart'])) { - foreach ($relations['haspart'] as $part_pageid => $exists) { - if ($exists) { - $dependencies[] = wikiFN($part_pageid); - } - } - } - } - - $dependencies[] = metaFN($pageid, '.meta'); - } - - $depends['files'] = array_map('wikiFN', $this->list); - $depends['files'][] = __FILE__; - $depends['files'][] = __DIR__ . '/renderer.php'; - $depends['files'][] = __DIR__ . '/mpdf/mpdf.php'; - $depends['files'] = array_merge( - $depends['files'], - $dependencies, - getConfigFiles('main') - ); - return $cache; - } - - /** - * Returns the parsed Wikitext in dw2pdf for the given id and revision - * - * @param string $id page id - * @param string|int $rev revision timestamp or empty string - * @param string $date_at - * @return null|string - */ - protected function wikiToDW2PDF($id, $rev = '', $date_at = '') - { - $file = wikiFN($id, $rev); - - if (!file_exists($file)) { - return ''; - } - - //ensure $id is in global $ID (needed for parsing) - global $ID; - $keep = $ID; - $ID = $id; - - if ($rev || $date_at) { - //no caching on old revisions - $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $id, $rev)), $info, $date_at); - } else { - $ret = p_cached_output($file, 'dw2pdf', $id); - } - - //restore ID (just in case) - $ID = $keep; - - return $ret; - } - - /** - * Build a pdf from the html - * - * @param string $cachefile - * @param Event $event - * @throws MpdfException - */ - protected function generatePDF($cachefile, $event) - { - global $REV, $INPUT, $DATE_AT; - - if ($event->data == 'export_pdf') { //only one page is exported - $rev = $REV; - $date_at = $DATE_AT; - } else { - //we are exporting entire namespace, ommit revisions - $rev = ''; - $date_at = ''; - } - - //some shortcuts to export settings - $hasToC = $this->getExportConfig('hasToC'); - $levels = $this->getExportConfig('levels'); - $isDebug = $this->getExportConfig('isDebug'); - $watermark = $this->getExportConfig('watermark'); - - // initialize PDF library - require_once(__DIR__ . "/DokuPDF.class.php"); - - $mpdf = new DokuPDF( - $this->getExportConfig('pagesize'), - $this->getExportConfig('orientation'), - $this->getExportConfig('font-size'), - $this->getDocumentLanguage($this->list[0]) //use language of first page - ); - - // let mpdf fix local links - $self = parse_url(DOKU_URL); - $url = $self['scheme'] . '://' . $self['host']; - if (!empty($self['port'])) { - $url .= ':' . $self['port']; - } - $mpdf->SetBasePath($url); - - // Set the title - $mpdf->SetTitle($this->title); - - // some default document settings - //note: double-sided document, starts at an odd page (first page is a right-hand side page) - // single-side document has only odd pages - $mpdf->mirrorMargins = $this->getExportConfig('doublesided'); - $mpdf->setAutoTopMargin = 'stretch'; - $mpdf->setAutoBottomMargin = 'stretch'; -// $mpdf->pagenumSuffix = '/'; //prefix for {nbpg} - if ($hasToC) { - $mpdf->h2toc = $levels; - } - $mpdf->PageNumSubstitutions[] = ['from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off']; - - // Watermarker - if ($watermark) { - $mpdf->SetWatermarkText($watermark); - $mpdf->showWatermarkText = true; - } - - // load the template - $template = $this->loadTemplate(); - - // prepare HTML header styles - $html = ''; - if ($isDebug) { - $html .= ''; - $html .= ''; - $html .= ''; - } - - $body_start = $template['html']; - $body_start .= '
'; - - // insert the cover page - $body_start .= $template['cover']; - - $mpdf->WriteHTML($body_start, 2, true, false); //start body html - if ($isDebug) { - $html .= $body_start; - } - if ($hasToC) { - //Note: - for double-sided document the ToC is always on an even number of pages, so that the - // following content is on a correct odd/even page - // - first page of ToC starts always at odd page (so eventually an additional blank page - // is included before) - // - there is no page numbering at the pages of the ToC - $mpdf->TOCpagebreakByArray([ - 'toc-preHTML' => '

' . $this->getLang('tocheader') . '

', - 'toc-bookmarkText' => $this->getLang('tocheader'), - 'links' => true, - 'outdent' => '1em', - 'pagenumstyle' => '1' - ]); - $html .= ''; - } - - // loop over all pages - $counter = 0; - $no_pages = count($this->list); - foreach ($this->list as $page) { - $this->currentBookChapter = $counter; - $counter++; - - $pagehtml = $this->wikiToDW2PDF($page, $rev, $date_at); - //file doesn't exists - if ($pagehtml == '') { - continue; - } - $pagehtml .= $this->pageDependReplacements($template['cite'], $page); - if ($counter < $no_pages) { - $pagehtml .= ''; - } - - $mpdf->WriteHTML($pagehtml, 2, false, false); //intermediate body html - if ($isDebug) { - $html .= $pagehtml; - } - } - - // insert the back page - $body_end = $template['back']; - - $body_end .= '
'; - - $mpdf->WriteHTML($body_end, 2, false); // finish body html - if ($isDebug) { - $html .= $body_end; - $html .= ''; - $html .= ''; - } - - //Return html for debugging - if ($isDebug) { - if ($INPUT->str('debughtml', 'text', true) == 'text') { - header('Content-Type: text/plain; charset=utf-8'); - } - echo $html; - exit(); - } - - // write to cache file - $mpdf->Output($cachefile, 'F'); - } - - /** - * @param string $cachefile - */ - protected function sendPDFFile($cachefile) - { - header('Content-Type: application/pdf'); - header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); - header('Pragma: public'); - http_conditionalRequest(filemtime($cachefile)); - global $INPUT; - $outputTarget = $INPUT->str('outputTarget', $this->getConf('output')); - - $filename = rawurlencode(cleanID(strtr($this->title, ':/;"', ' '))); - if ($outputTarget === 'file') { - header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); - } else { - header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); - } - - //Bookcreator uses jQuery.fileDownload.js, which requires a cookie. - header('Set-Cookie: fileDownload=true; path=/'); - - //try to send file, and exit if done - http_sendfile($cachefile); - - $fp = @fopen($cachefile, "rb"); - if ($fp) { - http_rangeRequest($fp, filesize($cachefile), 'application/pdf'); - } else { - header("HTTP/1.0 500 Internal Server Error"); - echo "Could not read file - bad permissions?"; - } - exit(); - } - - /** - * Load the various template files and prepare the HTML/CSS for insertion - * - * @return array - */ - protected function loadTemplate() - { - global $ID; - global $conf; - global $INFO; - - // this is what we'll return - $output = [ - 'cover' => '', - 'back' => '', - 'html' => '', - 'page' => '', - 'first' => '', - 'cite' => '', - ]; - - // prepare header/footer elements - $html = ''; - foreach (['header', 'footer'] as $section) { - foreach (['', '_odd', '_even', '_first'] as $order) { - $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/' . $section . $order . '.html'; - if (file_exists($file)) { - $html .= '' . DOKU_LF; - $html .= file_get_contents($file) . DOKU_LF; - $html .= '' . DOKU_LF; - - // register the needed pseudo CSS - if ($order == '_first') { - $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; - } elseif ($order == '_even') { - $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; - } elseif ($order == '_odd') { - $output['page'] .= 'odd-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; - } else { - $output['page'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; - } - } - } - } - - // prepare replacements - $replace = [ - '@PAGE@' => '{PAGENO}', - '@PAGES@' => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / ' - '@TITLE@' => hsc($this->title), - '@WIKI@' => $conf['title'], - '@WIKIURL@' => DOKU_URL, - '@DATE@' => dformat(time()), - '@USERNAME@' => $INFO['userinfo']['name'] ?? '', - '@BASE@' => DOKU_BASE, - '@INC@' => DOKU_INC, - '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', - '@TPLINC@' => DOKU_INC . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/' - ]; - - // set HTML element - $html = str_replace(array_keys($replace), array_values($replace), $html); - //TODO For bookcreator $ID (= bookmanager page) makes no sense - $output['html'] = $this->pageDependReplacements($html, $ID); - - // cover page - $coverfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/cover.html'; - if (file_exists($coverfile)) { - $output['cover'] = file_get_contents($coverfile); - $output['cover'] = str_replace(array_keys($replace), array_values($replace), $output['cover']); - $output['cover'] = $this->pageDependReplacements($output['cover'], $ID); - $output['cover'] .= ''; - } - - // cover page - $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/back.html'; - if (file_exists($backfile)) { - $output['back'] = ''; - $output['back'] .= file_get_contents($backfile); - $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']); - $output['back'] = $this->pageDependReplacements($output['back'], $ID); - } - - // citation box - $citationfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/citation.html'; - if (file_exists($citationfile)) { - $output['cite'] = file_get_contents($citationfile); - $output['cite'] = str_replace(array_keys($replace), array_values($replace), $output['cite']); - } - - return $output; - } - - /** - * @param string $raw code with placeholders - * @param string $id pageid - * @return string - */ - protected function pageDependReplacements($raw, $id) - { - global $REV, $DATE_AT; - - // generate qr code for this page - $qr_code = ''; - if ($this->getConf('qrcodescale')) { - $url = hsc(wl($id, '', '&', true)); - $size = (float)$this->getConf('qrcodescale'); - $qr_code = sprintf( - '', - $url, - $size - ); - } - // prepare replacements - $replace['@ID@'] = $id; - $replace['@UPDATE@'] = dformat(filemtime(wikiFN($id, $REV))); - - $params = []; - if ($DATE_AT) { - $params['at'] = $DATE_AT; - } elseif ($REV) { - $params['rev'] = $REV; - } - $replace['@PAGEURL@'] = wl($id, $params, true, "&"); - $replace['@QRCODE@'] = $qr_code; - - $content = $raw; - - // let other plugins define their own replacements - $evdata = ['id' => $id, 'replace' => &$replace, 'content' => &$content]; - $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata); - if ($event->advise_before()) { - $content = str_replace(array_keys($replace), array_values($replace), $raw); - } - - // plugins may post-process HTML, e.g to clean up unused replacements - $event->advise_after(); - - // @DATE([, ])@ - $content = preg_replace_callback( - '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', - [$this, 'replaceDate'], - $content - ); - - return $content; - } + $event->preventDefault(); + $event->stopPropagation(); - /** - * (callback) Replace date by request datestring - * e.g. '%m(30-11-1975)' is replaced by '11' - * - * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern - * @return string - */ - public function replaceDate($match) - { - global $conf; - //no 2nd argument for default date format - if ($match[2] == null) { - $match[2] = $conf['dformat']; - } - return strftime($match[2], strtotime($match[1])); - } - - /** - * Load all the style sheets and apply the needed replacements - * - * @return string css styles - */ - protected function loadCSS() - { - global $conf; - //reuse the CSS dispatcher functions without triggering the main function - define('SIMPLE_TEST', 1); - require_once(DOKU_INC . 'lib/exe/css.php'); + $this->loadConfig(); + $config = new Config($this->conf); + $collector = CollectorFactory::create($event->data, $config, ((int) $REV) ?: null, ((int) $DATE_AT) ?: null); + $cache = new Cache($config, $collector); - // prepare CSS files - $files = array_merge( - [ - DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/', - DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/', - ], - $this->cssPluginPDFstyles(), - [ - DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', - DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css' => - DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', - DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', - ] + $pdfService = new PdfExportService( + $config, + $collector, + $cache, + $this->getLang('tocheader'), + $INPUT->server->str('REMOTE_USER', '', true) ); - $css = ''; - foreach ($files as $file => $location) { - $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); - $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; - $css .= css_loadfile($file, $location); - } - // apply pattern replacements - if (function_exists('css_styleini')) { - // compatiblity layer for pre-Greebo releases of DokuWiki - $styleini = css_styleini($conf['template']); - } else { - // Greebo functionality - $styleUtils = new StyleUtils(); - $styleini = $styleUtils->cssStyleini($conf['template']); // older versions need still the template - } - $css = css_applystyle($css, $styleini['replacements']); - - // parse less - return css_parseless($css); + $cacheFile = $pdfService->getPdf(); // dumps HTML when in debug mode and exits + $pdfService->sendPdf($cacheFile); //exits after sending } - /** - * Returns a list of possible Plugin PDF Styles - * - * Checks for a pdf.css, falls back to print.css - * - * @author Andreas Gohr - */ - protected function cssPluginPDFstyles() - { - $list = []; - $plugins = plugin_list(); - - $usestyle = explode(',', $this->getConf('usestyles')); - foreach ($plugins as $p) { - if (in_array($p, $usestyle)) { - $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/"; - $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/"; - - $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/"; - $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/"; - } - - $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/"; - $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/"; - - if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) { - $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/"; - $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/"; - } else { - $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/"; - $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/"; - } - } - - // template support - foreach ( - [ - 'pdf.css', - 'pdf.less', - 'css/pdf.css', - 'css/pdf.less', - 'styles/pdf.css', - 'styles/pdf.less' - ] as $file - ) { - if (file_exists(tpl_incdir() . $file)) { - $list[tpl_incdir() . $file] = tpl_basedir() . $file; - } - } - - return $list; - } - - /** - * Returns array of pages which will be included in the exported pdf - * - * @return array - */ - public function getExportedPages() - { - return $this->list; - } - - /** - * usort callback to sort by file lastmodified time - * - * @param array $a - * @param array $b - * @return int - */ - public function cbDateSort($a, $b) - { - if ($b['rev'] < $a['rev']) { - return -1; - } - if ($b['rev'] > $a['rev']) { - return 1; - } - return strcmp($b['id'], $a['id']); - } - - /** - * usort callback to sort by page id - * @param array $a - * @param array $b - * @return int - */ - public function cbPagenameSort($a, $b) - { - global $conf; - - $partsA = explode(':', $a['id']); - $countA = count($partsA); - $partsB = explode(':', $b['id']); - $countB = count($partsB); - $max = max($countA, $countB); - - - // compare namepsace by namespace - for ($i = 0; $i < $max; $i++) { - $partA = $partsA[$i] ?: null; - $partB = $partsB[$i] ?: null; - - // have we reached the page level? - if ($i === ($countA - 1) || $i === ($countB - 1)) { - // start page first - if ($partA == $conf['start']) { - return -1; - } - if ($partB == $conf['start']) { - return 1; - } - } - - // prefer page over namespace - if ($partA === $partB) { - if (!isset($partsA[$i + 1])) { - return -1; - } - if (!isset($partsB[$i + 1])) { - return 1; - } - continue; - } - - - // simply compare - return strnatcmp($partA, $partB); - } - - return strnatcmp($a['id'], $b['id']); - } - - /** - * Collects settings from: - * 1. url parameters - * 2. plugin config - * 3. global config - */ - protected function loadExportConfig() - { - global $INPUT; - global $conf; - - $this->exportConfig = []; - - // decide on the paper setup from param or config - $this->exportConfig['pagesize'] = $INPUT->str('pagesize', $this->getConf('pagesize'), true); - $this->exportConfig['orientation'] = $INPUT->str('orientation', $this->getConf('orientation'), true); - - // decide on the font-size from param or config - $this->exportConfig['font-size'] = $INPUT->str('font-size', $this->getConf('font-size'), true); - - $doublesided = $INPUT->bool('doublesided', (bool)$this->getConf('doublesided')); - $this->exportConfig['doublesided'] = $doublesided ? '1' : '0'; - - $this->exportConfig['watermark'] = $INPUT->str('watermark', ''); - - $hasToC = $INPUT->bool('toc', (bool)$this->getConf('toc')); - $levels = []; - if ($hasToC) { - $toclevels = $INPUT->str('toclevels', $this->getConf('toclevels'), true); - [$top_input, $max_input] = array_pad(explode('-', $toclevels, 2), 2, ''); - [$top_conf, $max_conf] = array_pad(explode('-', $this->getConf('toclevels'), 2), 2, ''); - $bounds_input = [ - 'top' => [ - (int)$top_input, - (int)$top_conf - ], - 'max' => [ - (int)$max_input, - (int)$max_conf - ] - ]; - $bounds = [ - 'top' => $conf['toptoclevel'], - 'max' => $conf['maxtoclevel'] - - ]; - foreach ($bounds_input as $bound => $values) { - foreach ($values as $value) { - if ($value > 0 && $value <= 5) { - //stop at valid value and store - $bounds[$bound] = $value; - break; - } - } - } - - if ($bounds['max'] < $bounds['top']) { - $bounds['max'] = $bounds['top']; - } - - for ($level = $bounds['top']; $level <= $bounds['max']; $level++) { - $levels["H$level"] = $level - 1; - } - } - $this->exportConfig['hasToC'] = $hasToC; - $this->exportConfig['levels'] = $levels; - - $this->exportConfig['maxbookmarks'] = $INPUT->int('maxbookmarks', $this->getConf('maxbookmarks'), true); - - $tplconf = $this->getConf('template'); - $tpl = $INPUT->str('tpl', $tplconf, true); - if (!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) { - $tpl = $tplconf; - } - if (!$tpl) { - $tpl = 'default'; - } - $this->exportConfig['template'] = $tpl; - - $this->exportConfig['isDebug'] = $conf['allowdebug'] && $INPUT->has('debughtml'); - } - - /** - * Returns requested config - * - * @param string $name - * @param mixed $notset - * @return mixed|bool - */ - public function getExportConfig($name, $notset = false) - { - if ($this->exportConfig === null) { - $this->loadExportConfig(); - } - - return $this->exportConfig[$name] ?? $notset; - } - - /** - * Add 'export pdf'-button to pagetools - * - * @param Event $event - */ - public function addbutton(Event $event) - { - global $ID, $REV, $DATE_AT; - - if ($this->getConf('showexportbutton') && $event->data['view'] == 'main') { - $params = ['do' => 'export_pdf']; - if ($DATE_AT) { - $params['at'] = $DATE_AT; - } elseif ($REV) { - $params['rev'] = $REV; - } - - // insert button at position before last (up to top) - $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + - ['export_pdf' => sprintf( - '
  • %s
  • ', - wl($ID, $params), - 'action export_pdf', - $this->getLang('export_pdf_button'), - $this->getLang('export_pdf_button') - )] + - array_slice($event->data['items'], -1, 1, true); - } - } /** * Add 'export pdf' button to page tools, new SVG based mechanism @@ -1096,27 +88,4 @@ public function addsvgbutton(Event $event) array_splice($event->data['items'], -1, 0, [new MenuItem()]); } - - /** - * Get the language of the current document - * - * Uses the translation plugin if available - * @return string - */ - protected function getDocumentLanguage($pageid) - { - global $conf; - - $lang = $conf['lang']; - /** @var helper_plugin_translation $trans */ - $trans = plugin_load('helper', 'translation'); - if ($trans) { - $tr = $trans->getLangPart($pageid); - if ($tr) { - $lang = $tr; - } - } - - return $lang; - } } diff --git a/composer.json b/composer.json index 65cf6e8b..0b526e85 100644 --- a/composer.json +++ b/composer.json @@ -8,12 +8,15 @@ ], "config": { "platform": { - "php": "7.2" + "php": "8.0" } }, "require": { - "mpdf/mpdf": "8.0.*", - "mpdf/qrcode": "^1.2" + "ext-dom": "*", + "ext-libxml": "*", + "mpdf/mpdf": "8.2.*", + "mpdf/qrcode": "^1.2", + "psr/log": "^1.0" }, "replace": { "paragonie/random_compat": "*" diff --git a/composer.lock b/composer.lock index c66b74c8..30af5c10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,36 +4,39 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7b00711d12ac1ef73bc21f9046c21eea", + "content-hash": "70e3f425d861b01c41a304e53af0cadc", "packages": [ { "name": "mpdf/mpdf", - "version": "v8.0.17", + "version": "v8.2.7", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { "ext-gd": "*", "ext-mbstring": "*", + "mpdf/psr-http-message-shim": "^1.0 || ^2.0", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0", - "psr/log": "^1.0 || ^2.0", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" }, "require-dev": { "mockery/mockery": "^1.3.0", "mpdf/qrcode": "^1.1.0", "squizlabs/php_codesniffer": "^3.5.0", - "tracy/tracy": "^2.4", + "tracy/tracy": "~2.5", "yoast/phpunit-polyfills": "^1.0" }, "suggest": { @@ -43,6 +46,9 @@ }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { "Mpdf\\": "src/" } @@ -69,7 +75,7 @@ "utf-8" ], "support": { - "docs": "http://mpdf.github.io", + "docs": "https://mpdf.github.io", "issues": "https://github.com/mpdf/mpdf/issues", "source": "https://github.com/mpdf/mpdf" }, @@ -79,20 +85,112 @@ "type": "custom" } ], - "time": "2022-01-20T10:51:40+00:00" + "time": "2025-12-01T10:18:02+00:00" + }, + { + "name": "mpdf/psr-http-message-shim", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-http-message-shim.git", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f", + "shasum": "" + }, + "require": { + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrHttpMessageShim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + }, + { + "name": "Nigel Cunningham", + "email": "nigel.cunningham@technocrat.com.au" + } + ], + "description": "Shim to allow support of different psr/message versions.", + "support": { + "issues": "https://github.com/mpdf/psr-http-message-shim/issues", + "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1" + }, + "time": "2023-10-02T14:34:03+00:00" + }, + { + "name": "mpdf/psr-log-aware-trait", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ], + "description": "Trait to allow support of different psr/log versions.", + "support": { + "issues": "https://github.com/mpdf/psr-log-aware-trait/issues", + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0" + }, + "time": "2023-05-03T06:18:28+00:00" }, { "name": "mpdf/qrcode", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/mpdf/qrcode.git", - "reference": "0c09fce8b28707611c3febdd1ca424d40f172184" + "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/qrcode/zipball/0c09fce8b28707611c3febdd1ca424d40f172184", - "reference": "0c09fce8b28707611c3febdd1ca424d40f172184", + "url": "https://api.github.com/repos/mpdf/qrcode/zipball/5320c512776aa3c199bd8be8f707ec83d9779d85", + "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85", "shasum": "" }, "require": { @@ -139,7 +237,7 @@ ], "support": { "issues": "https://github.com/mpdf/qrcode/issues", - "source": "https://github.com/mpdf/qrcode/tree/v1.2.0" + "source": "https://github.com/mpdf/qrcode/tree/v1.2.1" }, "funding": [ { @@ -147,41 +245,43 @@ "type": "custom" } ], - "time": "2022-01-11T09:42:41+00:00" + "time": "2024-06-04T13:40:39+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -195,20 +295,83 @@ "object", "object graph" ], - "time": "2020-06-29T13:22:24+00:00" + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -232,7 +395,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -242,34 +405,38 @@ "psr", "psr-3" ], - "time": "2020-03-23T09:12:05+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" }, { "name": "setasign/fpdi", - "version": "v2.3.3", + "version": "v2.6.4", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "50c388860a73191e010810ed57dbed795578e867" + "reference": "4b53852fde2734ec6a07e458a085db627c60eada" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/50c388860a73191e010810ed57dbed795578e867", - "reference": "50c388860a73191e010810ed57dbed795578e867", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^5.6 || ^7.0" + "php": "^7.1 || ^8.0" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "~5.7", - "setasign/fpdf": "~1.8", - "setasign/tfpdf": "1.31", - "tecnickcom/tcpdf": "~6.2" + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" }, "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." @@ -303,19 +470,32 @@ "fpdi", "pdf" ], - "time": "2020-04-28T12:40:35+00:00" + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2025-08-05T09:57:14+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": { + "ext-dom": "*", + "ext-libxml": "*" + }, + "platform-dev": {}, "platform-overrides": { - "php": "7.2" + "php": "8.0" }, "plugin-api-version": "2.6.0" } diff --git a/conf/default.php b/conf/default.php index 77aae8d1..df4ff749 100644 --- a/conf/default.php +++ b/conf/default.php @@ -8,8 +8,9 @@ $conf['toclevels'] = ''; $conf['headernumber'] = 0; $conf['maxbookmarks'] = 5; +$conf['watermark'] = ''; $conf['template'] = 'default'; -$conf['output'] = 'file'; +$conf['output'] = 'browser'; $conf['usecache'] = 1; $conf['usestyles'] = 'wrap,'; $conf['qrcodescale'] = '1'; diff --git a/conf/metadata.php b/conf/metadata.php index 321242f0..ec08ae1c 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -8,6 +8,7 @@ $meta['toclevels'] = array('string', '_pattern' => '/^(|[1-5]-[1-5])$/'); $meta['headernumber'] = array('onoff'); $meta['maxbookmarks'] = array('numeric'); +$meta['watermark'] = array('string'); $meta['template'] = array('dirchoice', '_dir' => DOKU_PLUGIN . 'dw2pdf/tpl/'); $meta['output'] = array('multichoice', '_choices' => array('browser', 'file')); $meta['usecache'] = array('onoff'); diff --git a/lang/en/settings.php b/lang/en/settings.php index facc067c..56538f62 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -16,6 +16,7 @@ $lang['toclevels'] = 'Define top level and maximum level depth which are added to ToC. Default wiki ToC levels toptoclevel and maxtoclevel are used. Format: <top>-<max>'; $lang['headernumber'] = 'Activate numbered headings'; $lang['maxbookmarks'] = 'How many section levels should be used in the PDF bookmarks? (0=none, 5=all)'; +$lang['watermark'] = 'Optional watermark text to show on each page (e.g. "CONFIDENTIAL")'; $lang['template'] = 'Which template should be used for formatting the PDFs?'; $lang['output'] = 'How should the PDF be presented to the user?'; $lang['output_o_browser'] = 'Show in browser'; diff --git a/plugin.info.txt b/plugin.info.txt index 9a027e52..5eb53355 100644 --- a/plugin.info.txt +++ b/plugin.info.txt @@ -5,4 +5,5 @@ date 2023-11-25 name Dw2Pdf plugin desc DokuWiki to PDF converter url https://www.dokuwiki.org/plugin:dw2pdf +phpmin 8.0 diff --git a/renderer.php b/renderer.php index 8315c864..6e553f52 100644 --- a/renderer.php +++ b/renderer.php @@ -2,6 +2,8 @@ // phpcs:disable: PSR1.Methods.CamelCapsMethodName.NotCamelCaps // phpcs:disable: PSR2.Methods.MethodDeclaration.Underscore +use dokuwiki\plugin\dw2pdf\src\AbstractCollector; +use dokuwiki\plugin\dw2pdf\src\Config; /** * DokuWiki Plugin dw2pdf (Renderer Component) @@ -15,38 +17,52 @@ class renderer_plugin_dw2pdf extends Doku_Renderer_xhtml private $lastHeaderLevel = -1; private $originalHeaderLevel = 0; private $difference = 0; - private static $header_count = []; - private static $previous_level = 0; + private $header_count = []; + private $previous_level = 0; + private int $chapter = 0; + private ?Config $config; /** - * Stores action instance + * The Writer will reinitialize the renderer for each export, but the object will be reused within one export. * - * @var action_plugin_dw2pdf + * @inheritdoc */ - private $actioninstance; + public function isSingleton() + { + return true; + } /** - * load action plugin instance + * Set the active configuration + * + * @param Config $config + * @return void */ - public function __construct() + public function setConfig(Config $config): void { - $this->actioninstance = plugin_load('action', 'dw2pdf'); + $this->config = $config; } public function document_start() { global $ID; + if($this->config === null) { + throw new RuntimeException('DW2PDF Renderer configuration not set'); + } + parent::document_start(); - //ancher for rewritten links to included pages + //anchor for rewritten links to included pages $check = false; $pid = sectionID($ID, $check); $this->doc .= ""; $this->doc .= ""; - self::$header_count[1] = $this->actioninstance->getCurrentBookChapter(); + + $this->header_count[1] = $this->chapter; + $this->chapter++; } /** @@ -89,28 +105,31 @@ public function header($text, $level, $pos, $returnonly = false) // retrieve numbered headings option - $isnumberedheadings = $this->actioninstance->getExportConfig('headernumber'); + $isnumberedheadings = $this->config->useNumberedHeaders(); - $header_prefix = ""; + $header_prefix = ''; if ($isnumberedheadings) { if ($level > 0) { - if (self::$previous_level > $level) { - for ($i = $level + 1; $i <= self::$previous_level; $i++) { - self::$header_count[$i] = 0; + if ($this->previous_level > $level) { + for ($i = $level + 1; $i <= $this->previous_level; $i++) { + $this->header_count[$i] = 0; } } } - self::$header_count[$level]++; + $this->header_count[$level] = ($this->header_count[$level] ?? 0) + 1; // $header_prefix = ""; for ($i = 1; $i <= $level; $i++) { - $header_prefix .= self::$header_count[$i] . "."; + $header_prefix .= $this->header_count[$i] . "."; } } + if($header_prefix !== '') { + $header_prefix .= ' '; + } // add PDF bookmark $bookmark = ''; - $maxbookmarklevel = $this->actioninstance->getExportConfig('maxbookmarks'); + $maxbookmarklevel = $this->config->getMaxBookmarks(); // 0: off, 1-6: show down to this level if ($maxbookmarklevel && $maxbookmarklevel >= $level) { $bookmarklevel = $this->calculateBookmarklevel($level); @@ -128,7 +147,7 @@ public function header($text, $level, $pos, $returnonly = false) $this->doc .= $this->_xmlEntities($text); $this->doc .= ""; $this->doc .= "" . DOKU_LF; - self::$previous_level = $level; + $this->previous_level = $level; } /** @@ -238,19 +257,29 @@ public function acronym($acronym) /** * reformat links if needed * + * Because the output of this renderer will be cached, but might be part of a larger PDF + * including multiple pages, the links are not rewritten here. + * Instead they will be rewritten in the created HTML after rendering but before feeding to mPDF. + * * @param array $link * @return string */ public function _formatLink($link) { - - // for internal links contains the title the pageid - if (in_array($link['title'], $this->actioninstance->getExportedPages())) { - [/* url */, $hash] = sexplode('#', $link['url'], 2, ''); - - $check = false; - $pid = sectionID($link['title'], $check); - $link['url'] = "#" . $pid . '__' . $hash; + // mark internal wiki links for later processing by the writer + if ( + !empty($link['more']) && + str_contains($link['more'], 'data-wiki-id=') + ) { + [, $hash] = sexplode('#', $link['url'], 2, ''); + $target = $link['title'] ?? ''; // for internal links, 'title' holds the target page id + if ($target !== '') { + $attrs = ['data-dw2pdf-target="' . hsc($target) . '"']; + if ($hash !== '') { + $attrs[] = 'data-dw2pdf-hash="' . hsc($hash) . '"'; + } + $link['more'] = trim($link['more'] . ' ' . implode(' ', $attrs)); + } } // prefix interwiki links with interwiki icon @@ -277,7 +306,6 @@ public function _formatLink($link) * no obfuscation for email addresses * * @param string $address - * @param null $name * @param bool $returnonly * @return string|void */ diff --git a/src/AbstractCollector.php b/src/AbstractCollector.php new file mode 100644 index 00000000..9c5c293b --- /dev/null +++ b/src/AbstractCollector.php @@ -0,0 +1,143 @@ +config = $config; + $this->rev = $rev; + $this->at = $at; + $this->title = $config->getBookTitle() ?? ''; + + // collected pages are cleaned and checked for read access + $this->pages = array_filter( + array_map('cleanID', $this->collect()), + fn($page) => auth_quickaclcheck($page) >= AUTH_READ + ); + } + + /** + * Get the combined configuration/request context for this export. + * + * @return Config + */ + protected function getConfig(): Config + { + return $this->config; + } + + /** + * Collect the pages to be included in the PDF + * + * The collected pages will be cleaned and checked for read access automatically. + * + * This method should check for page existence, though (might depend on $rev/$at). + * + * @return string[] The list of page ids + */ + abstract protected function collect(): array; + + /** + * Get the title to be used for the PDF + * + * @return string + */ + public function getTitle(): string + { + if (!$this->title && $this->pages) { + $this->title = p_get_first_heading($this->pages[0]) ?: noNS($this->pages[0]); + } + + if (!$this->title) { + $this->title = 'PDF Export'; + } + + return $this->title; + } + + /** + * Get the language to be used for the PDF + * + * Use the language of the first page if possible, otherwise fall back to the default language + * + * @return string + */ + public function getLanguage() + { + global $conf; + + $lang = $conf['lang']; + if ($this->pages == []) return $lang; + + + /** @var helper_plugin_translation $trans */ + $trans = plugin_load('helper', 'translation'); + if (!$trans) return $lang; + $tr = $trans->getLangPart($this->pages[0]); + if ($tr) return $tr; + + return $lang; + } + + /** + * Get the set revision if any + * + * @return int|null + */ + public function getRev(): ?int + { + return $this->rev; + } + + /** + * Get the set dateat timestamp if any + * + * @return int|null + */ + public function getAt(): ?int + { + return $this->at; + } + + /** + * Get the list of page ids to include in the PDF + * + * @return string[] + */ + public function getPages(): array + { + return $this->pages; + } + + /** + * Get the list of file paths to include in the PDF + * + * Handles $rev if set + * + * @return string[] + * @todo no handling of $at yet + */ + public function getFiles(): array + { + return array_map(fn($id) => wikiFN($id, $this->rev), $this->pages); + } +} diff --git a/src/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php new file mode 100644 index 00000000..057c2dba --- /dev/null +++ b/src/BookCreatorLiveSelectionCollector.php @@ -0,0 +1,18 @@ +getConfig()->getLiveSelection(); + if ($selection === null) return []; + $list = (array)json_decode($selection, true, 512, JSON_THROW_ON_ERROR); + return array_filter($list, fn($page) => page_exists($page)); + } +} diff --git a/src/BookCreatorSavedSelectionCollector.php b/src/BookCreatorSavedSelectionCollector.php new file mode 100644 index 00000000..00720ddd --- /dev/null +++ b/src/BookCreatorSavedSelectionCollector.php @@ -0,0 +1,28 @@ +getConfig()->getSavedSelection(); + if ($savedSelectionId === null) return []; + + $savedselection = $bcPlugin->loadSavedSelection($savedSelectionId); + if (!$this->title && !empty($savedselection['title'])) { + $this->title = $savedselection['title']; + } + + $list = (array) $savedselection['selection']; + return array_filter($list, fn($page) => page_exists($page)); + } +} diff --git a/src/Cache.php b/src/Cache.php new file mode 100644 index 00000000..004e16e2 --- /dev/null +++ b/src/Cache.php @@ -0,0 +1,90 @@ +collector = $collector; + + $pages = $collector->getPages(); + sort($pages); + $key = implode(':', [ + implode(',', $pages), + $config->getCacheKey(), + $collector->getTitle(), + ]); + + parent::__construct($key, '.dw2.pdf'); + + $this->addDependencies(); + } + + /** + * When this was a cache for a specific revision, remove it on destruction + */ + public function __destruct() + { + if (!$this->collector->getRev()) return; + $this->removeCache(); + } + + /** @inheritdoc */ + public function useCache($depends = []) + { + // when a specific revision is requested, do not use the cache + if ($this->collector->getRev()) { + return false; + } + + return parent::useCache($depends); + } + + /** + * Note: we do not set up any dependencies to the plugin source code itself. On normal installation, + * the config files will be touched which will invalidate the cache. During development, the developer + * should manually purge the cache when changing the plugin code. + * @inheritdoc + */ + protected function addDependencies() + { + parent::addDependencies(); + + // images and included pages + $dependencies = []; + foreach ($this->collector->getPages() as $pageid) { + $relations = p_get_metadata($pageid, 'relation'); + + if (is_array($relations)) { + if (array_key_exists('media', $relations) && is_array($relations['media'])) { + foreach ($relations['media'] as $mediaid => $exists) { + if ($exists) { + $dependencies[] = mediaFN($mediaid); + } + } + } + + if (array_key_exists('haspart', $relations) && is_array($relations['haspart'])) { + foreach ($relations['haspart'] as $part_pageid => $exists) { + if ($exists) { + $dependencies[] = wikiFN($part_pageid); + } + } + } + } + + $dependencies[] = metaFN($pageid, '.meta'); + } + + // set up the dependencies + $this->depends['files'] = array_merge( + $dependencies, + $this->collector->getFiles(), + getConfigFiles('main') + ); + } +} diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php new file mode 100644 index 00000000..650ccfba --- /dev/null +++ b/src/CollectorFactory.php @@ -0,0 +1,35 @@ +hasLiveSelection()) { + return new BookCreatorLiveSelectionCollector($config, $rev, $at); + } elseif ($config->hasSavedSelection()) { + return new BookCreatorSavedSelectionCollector($config, $rev, $at); + } + // fallthrough + default: + throw new \InvalidArgumentException('Invalid export configuration'); + } + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..a6375532 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,450 @@ +tempDir = $conf['tmpdir'] . '/mpdf'; + io_mkdir_p($this->tempDir); + + // set default ToC levels from main config + $this->tocLevels = $this->parseTocLevels($conf['toptoclevel'] . '-' . $conf['maxtoclevel']); + + $this->loadPluginConfig($pluginConf); + $this->loadInputConfig(); + } + + /** + * Set a property with type casting and custom parsing + * + * @param string $prop The property name to set + * @param string|null $type The property type + * @param mixed $value The value to set + * @return void + * @see loadPluginConfig + * @see loadInputConfig + */ + protected function setProperty(string $prop, ?string $type, $value) + { + // custom value parsing (lowercased property names to avoid mistakes) + $value = match (strtolower($prop)) { + 'islandscape' => ($value === 'landscape'), + 'toclevels' => is_array($value) ? $value : $this->parseTocLevels((string)$value), + 'exportid' => cleanID((string)$value), + default => $value, + }; + + // standard type casting + $this->$prop = match ($type) { + 'int' => (int)$value, + 'bool' => (bool)$value, + 'float' => (float)$value, + 'array' => is_array($value) + ? $value + : array_filter(array_map('trim', explode(',', (string)$value))), + default => $value, + }; + } + + /** + * Apply the given configuration + * + * This will set all properties annotated with FromConfig + * + * @param array $conf (Plugin) configuration + */ + public function loadPluginConfig(array $conf = []) + { + $reflection = new \ReflectionClass($this); + foreach ($reflection->getProperties() as $property) { + $attributes = $property->getAttributes(FromConfig::class); + if ($attributes === []) continue; + $attribute = $attributes[0]->newInstance(); // we only expect one + + $prop = $property->getName(); + $confName = $attribute->name ?? strtolower($prop); + $type = $property->getType()?->getName(); + + if (!isset($conf[$confName])) continue; + $this->setProperty($prop, $type, $conf[$confName]); + } + } + + /** + * Load configuration provided by INPUT parameters + * + * Not all parameters are overridable here + * + * @return void + */ + public function loadInputConfig() + { + global $INPUT, $ID; + + if ($ID) $this->exportId = $ID; // default exportId to current page ID + + $reflection = new \ReflectionClass($this); + foreach ($reflection->getProperties() as $property) { + $attributes = $property->getAttributes(FromInput::class); + if ($attributes === []) continue; + $attribute = $attributes[0]->newInstance(); // we only expect one + + $prop = $property->getName(); + $confName = $attribute->name ?? strtolower($prop); + $type = $property->getType()?->getName(); + + if (!$INPUT->has($confName)) continue; + $this->setProperty($prop, $type, $INPUT->param($confName)); + } + } + + /** + * Check whether ToC is enabled + * + * @return bool + */ + public function hasToc(): bool + { + return $this->hasToC; + } + + /** + * Check whether debug mode is enabled + * + * @return bool + */ + public function isDebugEnabled(): bool + { + return $this->isDebug; + } + + /** + * Get a list of extensions whose screen styles should be applied + * + * @return string[] + */ + public function getStyledExtensions(): array + { + return $this->useStyles; + } + + /** + * Get the name of the selected template + * + * @return string + */ + public function getTemplateName(): string + { + return $this->template; + } + + /** + * Get the maximum number of bookmarks to include + * + * @return int + */ + public function getMaxBookmarks(): int + { + return $this->maxBookmarks; + } + + /** + * Check whether numbered headers are to be used + * + * @return bool + */ + public function useNumberedHeaders(): bool + { + return $this->numberedHeaders; + } + + /** + * Get the QR code scale + * + * @return float + */ + public function getQRScale(): float + { + return $this->qrCodeScale; + } + + /** + * Get desired PDF delivery target (inline or file download) + * + * @return string + */ + public function getOutputTarget(): string + { + return $this->outputTarget; + } + + /** + * Get a unique cache key for the current configuration + * + * @return string + */ + public function getCacheKey(): string + { + return implode(',', [ + $this->template, + $this->pagesize, + $this->isLandscape ? 'L' : 'P', + $this->fontSize, + $this->isDoublesided ? 'D' : 'S', + $this->hasToC ? 'T' : 'N', + $this->maxBookmarks, + $this->numberedHeaders ? 'H' : 'N', + implode('-', $this->tocLevels) + ]); + } + + /** + * Parses the ToC levels configuration into an array + * + * @param string $toclevels eg. "2-4" + * @return array + */ + protected function parseTocLevels(string $toclevels): array + { + $levels = []; + [$top, $max] = sexplode('-', $toclevels, 2); + $top = max(1, min(5, (int)$top)); + $max = max(1, min(5, (int)$max)); + + if ($max < $top) { + $max = $top; + } + + for ($level = $top; $level <= $max; $level++) { + $levels["H$level"] = $level - 1; + } + + return $levels; + } + + + /** + * Return the paper format + * + * @return string + */ + public function getFormat() + { + $format = $this->pagesize; + if ($this->isLandscape) { + $format .= '-L'; + } + return $format; + } + + /** + * Return the watermark text if any + * + * @return string + */ + public function getWatermarkText(): string + { + return $this->watermark; + } + + /** + * Get all configuration for mpdf as array + * + * Note: mode and writing direction are set in DokuPDF based on the language + * + * @link https://mpdf.github.io/reference/mpdf-variables/overview.html + * @return array + */ + public function getMPdfConfig(): array + { + return [ + 'format' => $this->getFormat(), + 'default_font_size' => $this->fontSize, + 'tempDir' => $this->tempDir, + 'mirrorMargins' => $this->isDoublesided, + 'h2toc' => $this->hasToC ? $this->tocLevels : [], + 'showWatermarkText' => $this->watermark !== '', + + 'setAutoTopMargin' => 'stretch', + 'setAutoBottomMargin' => 'stretch', + 'autoScriptToLang' => true, + 'baseScript' => 1, + 'autoVietnamese' => true, + 'autoArabic' => true, + 'autoLangToFont' => true, + 'ignore_invalid_utf8' => true, + 'tabSpaces' => 4, + ]; + } + + /** + * Get the requested book title override if provided. + * + * @return string|null + */ + public function getBookTitle(): ?string + { + return $this->bookTitle; + } + + /** + * Get the namespace to export for namespace/book selections. + * + * @return string + */ + public function getBookNamespace(): string + { + return $this->bookNamespace; + } + + /** + * Get the page sort order selected for namespace exports. + * + * @return string + */ + public function getBookSortOrder(): string + { + return $this->bookSortOrder; + } + + /** + * Get the maximum namespace depth to traverse for namespace exports. + * + * @return int + */ + public function getBookNamespaceDepth(): int + { + return $this->bookNamespaceDepth; + } + + /** + * Get the list of explicitly excluded page IDs. + * + * @return string[] + */ + public function getBookExcludedPages(): array + { + return $this->bookExcludePages; + } + + /** + * Get the list of excluded namespaces. + * + * @return string[] + */ + public function getBookExcludedNamespaces(): array + { + return $this->bookExcludeNamespaces; + } + + /** + * Get the raw JSON payload representing a live book selection. + * + * @return string|null + */ + public function getLiveSelection(): ?string + { + return $this->liveSelection; + } + + /** + * Check whether a live selection payload was supplied. + * + * @return bool + */ + public function hasLiveSelection(): bool + { + return $this->liveSelection !== null; + } + + /** + * Get the identifier of a saved book selection. + * + * @return string|null + */ + public function getSavedSelection(): ?string + { + return $this->savedSelection; + } + + /** + * Check whether a saved selection identifier was supplied. + * + * @return bool + */ + public function hasSavedSelection(): bool + { + return $this->savedSelection !== null; + } + + /** + * Get the requested page ID for single page exports. + * + * @return string + */ + public function getExportId(): string + { + return $this->exportId; + } +} diff --git a/src/DokuPdf.php b/src/DokuPdf.php new file mode 100644 index 00000000..d8a826a4 --- /dev/null +++ b/src/DokuPdf.php @@ -0,0 +1,105 @@ + + */ +class DokuPdf extends Mpdf +{ + /** + * DokuPDF constructor. + * + * @param Config $config + * @param string $lang The language code to use for this document + * @throws MpdfException + */ + public function __construct(Config $config, string $lang) + { + $initConfig = $config->getMPdfConfig(); + $initConfig['mode'] = $this->lang2mode($lang); + + $container = new SimpleContainer([ + 'httpClient' => new HttpClient(), + 'localContentLoader' => new LocalContentLoader(), + ]); + + parent::__construct($initConfig, $container); + $this->SetDirectionality($this->lang2direction($lang)); + + // configure page numbering + // https://mpdf.github.io/paging/page-numbering.html + $this->PageNumSubstitutions[] = ['from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off']; + // add watermark text if configured + $this->setWatermarkText($config->getWatermarkText()); + + // let mpdf fix local links + $self = parse_url(DOKU_URL); + $url = $self['scheme'] . '://' . $self['host']; + if (!empty($self['port'])) { + $url .= ':' . $self['port']; + } + $this->SetBasePath($url); + } + + /** + * Decode all paths, since DokuWiki uses XHTML compliant URLs + * + * @inheritdoc + */ + public function GetFullPath(&$path, $basepath = '') + { + $path = htmlspecialchars_decode($path); + parent::GetFullPath($path, $basepath); + } + + /** + * Get the mode to use based on the given language + * + * @link https://mpdf.github.io/reference/mpdf-functions/construct.html + * @link https://mpdf.github.io/reference/mpdf-variables/useadobecjk.html + * @todo it might be more sensible to pass a language string instead + * @param string $lang + * @return string + */ + protected function lang2mode(string $lang): string + { + switch ($lang) { + case 'zh': + case 'zh-tw': + case 'ja': + case 'ko': + return '+aCJK'; + default: + return 'UTF-8-s'; + } + } + + /** + * Return the writing direction based on the set language + * + * @param string $lang + * @return string + */ + protected function lang2direction(string $lang): string + { + switch ($lang) { + case 'ar': + case 'he': + return 'rtl'; + default: + return 'ltr'; + } + } +} diff --git a/src/HttpClient.php b/src/HttpClient.php new file mode 100644 index 00000000..94d34131 --- /dev/null +++ b/src/HttpClient.php @@ -0,0 +1,89 @@ +getUri(); + + $url = (string)$uri; + + // short-circuit to cached/local copies whenever possible + $resolved = (new MediaLinkResolver())->resolve($url); + if ($resolved && is_readable($resolved['path'])) { + return (new Response()) + ->withStatus(200) + ->withHeader('Content-Type', $resolved['mime']) + ->withBody(Stream::create(file_get_contents($resolved['path']))); + } + + // fall back to the standard Dokuwiki HTTP client for any remote content + $client = new DokuHTTPClient(); + $client->headers = $this->buildHeaders($request); + $client->referer = $request->getHeaderLine('Referer'); + if ($agent = $request->getHeaderLine('User-Agent')) { + $client->agent = $agent; + } + + $body = (string)$request->getBody(); + if ($request->getBody()->isSeekable()) { + $request->getBody()->rewind(); + } + + $method = strtoupper($request->getMethod()); + $client->sendRequest($url, $body, $method); + + $response = (new Response())->withStatus($client->status ?: 500); + + foreach ((array)$client->resp_headers as $name => $value) { + if (is_array($value)) { + foreach ($value as $single) { + $response = $response->withHeader($name, $single); + } + } else { + $response = $response->withHeader($name, $value); + } + } + + return $response->withBody(Stream::create($client->resp_body)); + } + + /** + * Convert PSR-7 headers to the associative format expected by DokuHTTPClient. + * + * @param RequestInterface $request Original request from mPDF. + * @return array + */ + private function buildHeaders(RequestInterface $request): array + { + $headers = []; + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + return $headers; + } +} diff --git a/src/LocalContentLoader.php b/src/LocalContentLoader.php new file mode 100644 index 00000000..86f4e439 --- /dev/null +++ b/src/LocalContentLoader.php @@ -0,0 +1,33 @@ +resolve($path); + ; + if ($resolved) { + $path = $resolved['path']; + } + + if (!is_readable($path)) { + return null; + } + + return file_get_contents($path); + } +} diff --git a/src/MediaLinkResolver.php b/src/MediaLinkResolver.php new file mode 100644 index 00000000..a51ee085 --- /dev/null +++ b/src/MediaLinkResolver.php @@ -0,0 +1,186 @@ + string, 'mime' => string] when resolution succeeds, null otherwise. + */ + public function resolve(string $file): ?array + { + $mediaID = $this->extractMediaID($file); + if ($mediaID !== null) { + [$w, $h, $rev] = $this->extractMediaParams($file); + [$ext, $mime] = mimetype($mediaID); + if (!$ext) return null; + $localFile = $this->localMediaFile($mediaID, $ext, $rev); + if (!$localFile) return null; + if (str_starts_with($mime, 'image/')) { + $localFile = $this->resizedMedia($localFile, $ext, $w, $h); + } + } else { + [, $mime] = mimetype($file); + if (!str_starts_with($mime, 'image/')) return null; + $localFile = $this->extractLocalImage($file); + } + + if (!$localFile) return null; + return ['path' => $localFile, 'mime' => $mime]; + } + + /** + * Check if the given file URL corresponds to a Dokuwiki media ID and extract it. + * + * Handles rewritten media URLs (/media/*) and fetch.php calls by building a regex + * from the result of calling ml() for a fake media ID. + * + * Note that the returned media ID could still be an external URL! + * + * @param string $file + * @return string|null The extracted media ID, or null if not found. + */ + protected function extractMediaID(string $file): ?string + { + // build regex to parse URL back to media info (matches fetch.php calls) + $fetchRegex = preg_quote(ml('xxx123yyy', '', true, '&', true), '/'); + $fetchRegex = str_replace('xxx123yyy', '([^&\?]*)', $fetchRegex); + + // extract the real media from a fetch.php URI and determine mime + if ( + preg_match("/^$fetchRegex/", $file, $matches) || + preg_match('/[&?]media=([^&?]*)/', $file, $matches) + ) { + return rawurldecode($matches[1]); + } + + return null; + } + + /** + * Extract media parameters (width, height, revision) from the given file URL. + * + * When a parameter is not found, its value will be 0. + * + * @param string $file Source string (fetch call) + * @return array{int,int,int} Array containing width, height, and revision. + */ + protected function extractMediaParams(string $file): array + { + $width = $this->extractInt($file, 'w'); + $height = $this->extractInt($file, 'h'); + $rev = $this->extractInt($file, 'rev'); + return [$width, $height, $rev]; + } + + /** + * Returns a local, absolute path for the given media ID and revision + * + * This method will download external media files to the local cache if needed. ACLs are + * checked here as well. + * + * Returns null when the media file is not accessible. + * + * @param string $mediaID A media ID or external URL. + * @param string $ext File extension (used for external media caching). + * @param int $rev Revision number (0 for latest). + * @return string|null Absolute path to the local media file, or null when not accessible. + */ + protected function localMediaFile(string $mediaID, string $ext, int $rev): ?string + { + global $conf; + + if (media_isexternal($mediaID)) { + $local = media_get_from_URL($mediaID, $ext, $conf['cachetime']); + if (!$local) return null; + } else { + $mediaID = cleanID($mediaID); + // check permissions (namespace only) + if (auth_quickaclcheck(getNS($mediaID) . ':X') < AUTH_READ) { + return null; + } + $local = mediaFN($mediaID, $rev ?: ''); + } + if (!file_exists($local)) return null; + + return $local; + } + + /** + * Resize or crop the given media file as needed. + * + * @param string $mediaFile Absolute path to the local media file. + * @param string $ext File extension. + * @param int $width Desired width + * @param int $height Desired height + * @return string|null Absolute path to the resized/cropped media file, or null on failure. + */ + protected function resizedMedia($mediaFile, $ext, $width, $height) + { + if ($width && $height) { + $mediaFile = media_crop_image($mediaFile, $ext, $width, $height); + } elseif ($width || $height) { + $mediaFile = media_resize_image($mediaFile, $ext, $width, $height); + } + if (!file_exists($mediaFile)) return null; + return $mediaFile; + } + + /** + * Extract an integer parameter from the given subject URL. + * + * @param string $subject Source string, usually the media URL. + * @param string $param Name of the parameter to extract. + * @return int + */ + protected function extractInt(string $subject, string $param): int + { + $pattern = '/[?&]' . $param . '=(\d+)/'; + if (preg_match($pattern, $subject, $match)) { + return (int)$match[1]; + } + + return 0; + } + + /** + * Attempt to extract a local file path from the given URL. + * + * This only works for static files that are directly accessible on disk. Or our + * custom dw2pdf:// scheme for local files passed from plugins. + * + * @param string $file Source URL. + * @return string|null Absolute path to the local file, or null when not accessible. + */ + protected function extractLocalImage($file) + { + $local = null; + if (str_starts_with($file, 'dw2pdf://')) { + // support local files passed from plugins + $local = substr($file, 9); + } elseif (!preg_match('/(\.php|\?)/', $file)) { + // directly access local files instead of using HTTP (skipping dynamic content) + $base = preg_quote(DOKU_URL, '/'); + $local = preg_replace("/^$base/i", DOKU_INC, $file, 1); + } + + if (!file_exists($local)) return null; + if (!is_readable($local)) return null; + + return $local; + } +} diff --git a/src/NamespaceCollector.php b/src/NamespaceCollector.php new file mode 100644 index 00000000..026e5d62 --- /dev/null +++ b/src/NamespaceCollector.php @@ -0,0 +1,198 @@ +getConfig(); + + $this->namespace = $config->getBookNamespace(); + $this->sortorder = $config->getBookSortOrder(); + $this->depth = $config->getBookNamespaceDepth(); + if ($this->depth < 0) $this->depth = 0; + $this->excludePages = $config->getBookExcludedPages(); + $this->excludeNamespaces = $config->getBookExcludedNamespaces(); + + // check namespace exists + $nsdir = dirname(wikiFN($this->namespace . ':dummy')); + if (!@is_dir($nsdir)) throw new \Exception('needns'); + } + + /** + * @inheritdoc + * @triggers DW2PDF_NAMESPACEEXPORT_SORT + * @todo currently we do not support the 'at' parameter. We would need to search pages in the attic for this. + */ + protected function collect(): array + { + global $conf; + + try { + $this->initVars(); + } catch (\Exception $e) { + return []; + } + + //page search + $result = []; + $opts = ['depth' => $this->depth]; //recursive all levels + $dir = utf8_encodeFN(str_replace(':', '/', $this->namespace)); + search($result, $conf['datadir'], 'search_allpages', $opts, $dir); + + // remove excluded pages and namespaces + $result = $this->excludePages($result); + + + + // Sort pages, let plugins modify sorting + $eventData = ['pages' => &$result, 'sort' => $this->sortorder]; + $event = new Event('DW2PDF_NAMESPACEEXPORT_SORT', $eventData); + if ($event->advise_before()) { + $result = $this->sortPages($result); + } + $event->advise_after(); + + // extract page ids + $pages = array_column($result, 'id'); + + // if a there is a namespace start page outside the namespace, add it at the beginning + if ($this->namespace !== '') { + if (!in_array($this->namespace . ':' . $conf['start'], $pages, true)) { + if (file_exists(wikiFN(rtrim($this->namespace, ':')))) { + array_unshift($pages, rtrim($this->namespace, ':')); + } + } + } + + return $pages; + } + + /** + * Remove excluded pages and namespaces from the given list of pages + * + * @param array $pages The list of pages as returned by search() + * @return array The filtered list of pages + */ + protected function excludePages(array $pages) + { + $pages = array_filter($pages, fn($page) => !in_array($page['id'], $this->excludePages)); + $pages = array_filter($pages, function ($page) { + foreach ($this->excludeNamespaces as $ns) { + if (str_starts_with($page['id'], $ns . ':')) { + return false; + } + } + return true; + }); + return $pages; + } + + /** + * Sort the given list of pages according to the selected sort order + * + * @param array $pages The list of pages as returned by search() + * @return array The sorted list of pages + */ + protected function sortPages(array $pages): array + { + $sortoptions = ['pagename', 'date', 'natural']; + if (!in_array($this->sortorder, $sortoptions)) { + $this->sortorder = 'natural'; + } + + if ($this->sortorder == 'date') { + usort($pages, [$this, 'cbDateSort']); + } else { + usort($pages, [$this, 'cbPagenameSort']); + } + + return $pages; + } + + /** + * usort callback to sort by file lastmodified time + * + * @param array $a + * @param array $b + * @return int + */ + public function cbDateSort($a, $b) + { + if ($b['rev'] < $a['rev']) { + return -1; + } + if ($b['rev'] > $a['rev']) { + return 1; + } + return strcmp($b['id'], $a['id']); + } + + /** + * usort callback to sort by page id + * @param array $a + * @param array $b + * @return int + */ + public function cbPagenameSort($a, $b) + { + global $conf; + + $partsA = explode(':', $a['id']); + $countA = count($partsA); + $partsB = explode(':', $b['id']); + $countB = count($partsB); + $max = max($countA, $countB); + + + // compare namepsace by namespace + for ($i = 0; $i < $max; $i++) { + $partA = $partsA[$i] ?: null; + $partB = $partsB[$i] ?: null; + + // have we reached the page level? + if ($i === ($countA - 1) || $i === ($countB - 1)) { + // start page first + if ($partA == $conf['start']) { + return -1; + } + if ($partB == $conf['start']) { + return 1; + } + } + + // prefer page over namespace + if ($partA === $partB) { + if (!isset($partsA[$i + 1])) { + return -1; + } + if (!isset($partsB[$i + 1])) { + return 1; + } + continue; + } + + + // simply compare + return strnatcmp($partA, $partB); + } + + return strnatcmp($a['id'], $b['id']); + } +} diff --git a/src/PageCollector.php b/src/PageCollector.php new file mode 100644 index 00000000..9e02858a --- /dev/null +++ b/src/PageCollector.php @@ -0,0 +1,22 @@ +getConfig()->getExportId(); + if ($exportID === '') { + return []; + } + + // no export for non existing page + if (!page_exists($exportID, $this->rev)) { + return []; + } + + return [$exportID]; + } +} diff --git a/src/PdfExportService.php b/src/PdfExportService.php new file mode 100644 index 00000000..ac71daca --- /dev/null +++ b/src/PdfExportService.php @@ -0,0 +1,167 @@ +config = $config; + $this->collector = $collector; + $this->cache = $cache; + $this->tocHeader = $tocHeader; + $this->remoteUser = $remoteUser; + } + + /** + * Build the PDF (or return cached version) and provide its filesystem path. + * + * @return string + * @throws MpdfException + */ + public function getPdf(): string + { + if (!$this->cache->useCache() || $this->config->isDebugEnabled()) { + set_time_limit(0); + $this->buildDocument($this->cache->cache); + } + + return $this->cache->cache; + } + + /** + * Send the PDF to the browser. When $cacheFile is omitted, the PDF will be built (or loaded) first. + * + * @param string|null $cacheFile Absolute path to an already generated PDF file, if available + * @return void + * @throws MpdfException + */ + public function sendPdf(?string $cacheFile = null): void + { + $cacheFile ??= $this->getPdf(); + $title = $this->collector->getTitle(); + + header('Content-Type: application/pdf'); + header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); + header('Pragma: public'); + http_conditionalRequest(filemtime($cacheFile)); + + $outputTarget = $this->config->getOutputTarget(); + $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); + if ($outputTarget === 'file') { + header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); + } else { + header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); + } + + header('Set-Cookie: fileDownload=true; path=/'); + + http_sendfile($cacheFile); + + $fp = @fopen($cacheFile, 'rb'); + if ($fp) { + http_rangeRequest($fp, filesize($cacheFile), 'application/pdf'); + } else { + header('HTTP/1.0 500 Internal Server Error'); + echo 'Could not read file - bad permissions?'; + } + exit(); + } + + /** + * Build the PDF document and write it to the cache file. + * + * @param string $cacheFile Destination path for the generated PDF file + * @return void + * @throws MpdfException + */ + protected function buildDocument(string $cacheFile): void + { + $writer = $this->renderDocument(); + + if ($this->config->isDebugEnabled()) { + header('Content-Type: text/html; charset=utf-8'); + echo $writer->getDebugHTML(); + exit(); + } + + $writer->outputToFile($cacheFile); + } + + /** + * Build the PDF document without writing it to disk and expose the debug HTML. + * + * @return string Debug HTML collected while rendering the document + * @throws MpdfException + */ + public function getDebugHtml(): string + { + if (!$this->config->isDebugEnabled()) { + throw new \RuntimeException('Debug HTML is only available when debug mode is enabled'); + } + + return $this->renderDocument()->getDebugHTML(); + } + + /** + * Compose the document using the collector and return the writer. + * + * @return Writer Writer instance containing the rendered document + * @throws MpdfException + */ + protected function renderDocument(): Writer + { + $mpdf = new DokuPdf($this->config, $this->collector->getLanguage()); + $styles = new Styles($this->config); + $template = new Template($this->config); + $writer = new Writer($mpdf, $this->config, $template, $styles); + + $writer->startDocument($this->collector->getTitle()); + $writer->cover(); + + if ($this->config->hasToC()) { + $writer->toc($this->tocHeader); + } + + foreach ($this->collector->getPages() as $page) { + $template->setContext($this->collector, $page, $this->remoteUser); + $writer->renderWikiPage($this->collector, $page); + } + + $writer->back(); + $writer->endDocument(); + return $writer; + } +} diff --git a/src/Styles.php b/src/Styles.php new file mode 100644 index 00000000..d8d40c00 --- /dev/null +++ b/src/Styles.php @@ -0,0 +1,130 @@ +config = $config; + } + + /** + * Get the full CSS to include in the PDF + * + * Gathers all relevant CSS files, applies style replacements and parses LESS. + * + * @return string + */ + public function getCSS(): string + { + //reuse the CSS dispatcher functions without triggering the main function + if (!defined('SIMPLE_TEST')) { + define('SIMPLE_TEST', 1); + } + require_once(DOKU_INC . 'lib/exe/css.php'); + + // prepare CSS files + $files = $this->getStyleFiles(); + $css = ''; + foreach ($files as $file => $location) { + $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); + $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; + $css .= css_loadfile($file, $location); + } + + // apply style replacements + $styleUtils = new StyleUtils(); + $styleini = $styleUtils->cssStyleini(); + $css = css_applystyle($css, $styleini['replacements']); + + // parse less + return css_parseless($css); + } + + + /** + * Returns the list of style files to include in the PDF + * + * The array keys are the file paths on disk, the values are the + * paths as used inside the Styles (for resolving relative links). + * + * @return array + */ + protected function getStyleFiles(): array + { + $tpl = $this->config->getTemplateName(); + + return array_merge( + [ + DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/', + DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/', + ], + $this->getExtensionStyles(), + [ + DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', + DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $tpl . '/', + DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', + ] + ); + } + + /** + * Returns a list of possible Plugin and Template PDF Styles + * + * Checks for a pdf.css, falls back to print.css. For configured usestyles plugins + * the screen.css and style.css are also included. + */ + protected function getExtensionStyles() + { + $list = []; + $plugins = plugin_list(); + + $usestyle = $this->config->getStyledExtensions(); + foreach ($plugins as $p) { + if (in_array($p, $usestyle)) { + $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/"; + $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/"; + + $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/"; + $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/"; + } + + $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/"; + $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/"; + + if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) { + $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/"; + $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/"; + } else { + $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/"; + $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/"; + } + } + + // template support + foreach ( + [ + 'pdf.css', + 'pdf.less', + 'css/pdf.css', + 'css/pdf.less', + 'styles/pdf.css', + 'styles/pdf.less' + ] as $file + ) { + if (file_exists(tpl_incdir() . $file)) { + $list[tpl_incdir() . $file] = tpl_basedir() . $file; + } + } + + return $list; + } +} diff --git a/src/Template.php b/src/Template.php new file mode 100644 index 00000000..510a0801 --- /dev/null +++ b/src/Template.php @@ -0,0 +1,185 @@ + '', + 'rev' => '', + 'at' => '', + 'title' => '', + 'username' => '', + ]; + + + /** + * Constructor + * + * @param Config $config The DW2PDF configuration + */ + public function __construct(Config $config) + { + $this->name = $config->getTemplateName(); + $this->qrScale = $config->getQRScale(); + $this->dir = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->name; + if (!is_dir($this->dir)) { + throw new \RuntimeException("Template directory $this->dir does not exist"); + } + } + + /** + * Set the context for the current page + * + * This will be used in placeholders in headers/footers/cover/back pages. + * + * @param string $id The page ID + * @param string $title The title of the page + * @param string|null $rev The revision of the page (if any) + * @param string|null $at The dateat mechanism in use (if any) + * @param string|null $username The username of the user generating the PDF (if any) + * @return void + */ + public function setContext(AbstractCollector $collector, string $id, ?string $username): void + { + $this->context = [ + 'title' => $collector->getTitle(), + 'id' => $id, + 'rev' => $collector->getRev() ?? '', + 'at' => $collector->getAt() ?? '', + 'username' => $username ?? '', + ]; + } + + /** + * Get the HTML content for the given type and order + * + * Will fall back to non-ordered version if ordered version is not found. Placeholders + * will be replaced. + * + * @param string $type header, footer, cover, back, citation + * @param string $order first, even, odd or empty string for default + * @return string + */ + public function getHTML(string $type, string $order = ''): string + { + if ($order) $order = "_$order"; + + $file = $this->dir . '/' . $type . $order . '.html'; + if (!is_file($file)) $file = $this->dir . '/' . $type . '.html'; + if (!is_file($file)) return ''; + + $html = file_get_contents($file); + $html = $this->replacePlaceholders($html); + return $html; + } + + /** + * Applies the placeholder replacements to the given HTML + * + * Called for headers, footers, cover and back pages on each call. + * + * Accesses global DokuWiki variables to fill in page specific data. + * + * @triggers PLUGIN_DW2PDF_REPLACE + * @param string $html The template's HTML content + * @return string The HTML with placeholders replaced + */ + protected function replacePlaceholders(string $html): string + { + global $conf; + + $params = []; + if (!empty($this->context['at'])) { + $params['at'] = $this->context['at']; + } elseif (!empty($this->context['rev'])) { + $params['rev'] = $this->context['rev']; + } + $url = wl($this->context['id'], $params, true, "&"); + + $replace = [ + '@PAGE@' => '{PAGENO}', + '@PAGES@' => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / ' + '@TITLE@' => hsc($this->context['title'] ?? ''), + '@WIKI@' => $conf['title'], + '@WIKIURL@' => DOKU_URL, + '@DATE@' => dformat(time()), + '@USERNAME@' => hsc($this->context['username'] ?? ''), + '@BASE@' => DOKU_BASE, + '@INC@' => DOKU_INC, + '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->name . '/', + '@TPLINC@' => DOKU_INC . 'lib/plugins/dw2pdf/tpl/' . $this->name . '/', + // page dependent placeholders + '@ID' => $this->context['id'] ?? '', + '@UPDATE@' => dformat(filemtime(wikiFN($this->context['id'], $this->context['rev'] ?? ''))), + '@PAGEURL@' => $url, + '@QRCODE@' => $this->generateQRCode($url), + ]; + + // let other plugins define their own replacements + $evdata = [ + 'id' => $this->context['id'], + 'replace' => &$replace, + 'content' => &$html, + 'context' => $this->context + ]; + $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata); + if ($event->advise_before()) { + $html = str_replace(array_keys($replace), array_values($replace), $html); + } + // plugins may post-process HTML, e.g to clean up unused replacements + $event->advise_after(); + + // @DATE([, ])@ + $html = preg_replace_callback( + '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', + function ($match) { + global $conf; + //no 2nd argument for default date format + if ($match[2] == null) { + $match[2] = $conf['dformat']; + } + return strftime($match[2], strtotime($match[1])); + }, + $html + ); + + return $html; + } + + /** + * Generate QR code pseudo-HTML + * + * @param string $url The URL to encode + * @return string + */ + protected function generateQRCode($url): string + { + if ($this->qrScale <= 0.0) return ''; + + $url = hsc($url); + return sprintf( + '', + $url, + $this->qrScale + ); + } +} diff --git a/src/Writer.php b/src/Writer.php new file mode 100644 index 00000000..645440c7 --- /dev/null +++ b/src/Writer.php @@ -0,0 +1,440 @@ +mpdf = $mpdf; + $this->config = $config; + $this->template = $template; + $this->styles = $styles; + $this->debug = $config->isDebugEnabled(); + + /** + * initialize a new renderer instance (singleton instance will be reused in later p_* calls) + * @var \renderer_plugin_dw2pdf $renderer + */ + $renderer = plugin_load('renderer', 'dw2pdf', true); + $renderer->setConfig($config); + } + + /** + * Initialize the document + * + * @param string $title + * @return void + * @throws MpdfException + */ + public function startDocument(string $title): void + { + $this->mpdf->SetTitle($title); + + // Set the styles + $styles = '@page landscape-page { size:landscape }'; + $styles .= 'div.dw2pdf-landscape { page:landscape-page }'; + $styles .= '@page portrait-page { size:portrait }'; + $styles .= 'div.dw2pdf-portrait { page:portrait-page }'; + $styles .= $this->styles->getCSS(); + $this->write($styles, HTMLParserMode::HEADER_CSS); + + //start body html + $this->write('
    ', HTMLParserMode::HTML_BODY, true, false); + } + + /** + * Insert a page break + * + * @return void + * @throws MpdfException + */ + public function pageBreak(): void + { + $this->write('', 2, false, false); + } + + /** + * Write a wiki page into the PDF + * + * @param string $html The rendered HTML of the wiki page + * @return void + * @throws MpdfException + */ + public function wikiPage(string $html): void + { + $this->conditionalPageBreak(); + $this->applyHeaderFooters(); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); + + // add citation box if any + $cite = $this->template->getHTML('citation'); + if ($cite) { + $this->write($cite, HTMLParserMode::HTML_BODY, false, false); + } + + $this->breakAfterMe(); + } + + /** + * Render and write a wiki page into the PDF + * + * This caches the rendered page individually (unless a specific revision is requested). So even + * when PDF needs to be regenerated, pages that have not changed will be loaded from cache. + * + * @param AbstractCollector $collector The collector providing the page context + * @param string $pageId The page ID to render + * @return void + * @throws MpdfException + */ + public function renderWikiPage(AbstractCollector $collector, string $pageId): void + { + $rev = $collector->getRev(); + $at = $collector->getAt(); + $file = wikiFN($pageId,); + + //ensure $id is in global $ID (needed for parsing) + global $ID; + $keep = $ID; + $ID = $pageId; + + if ($collector->getRev()) { + //no caching on old revisions + $html = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $pageId, $rev)), $info, $at); + } else { + $html = p_cached_output($file, 'dw2pdf', $pageId); + } + + //restore ID (just in case) + $ID = $keep; + + // Fix internal links then write the page + $html = $this->fixInternalLinks($collector, $html); + $this->wikiPage($html); + } + + /** + * If the given HTML contains internal links to pages that are part of the exported PDF, + * fix the links to point to the correct section within the PDF. + * + * @param AbstractCollector $collector + * @param string $html The rendered HTML of the wiki page + * @return string + */ + protected function fixInternalLinks(AbstractCollector $collector, string $html): string + { + if ($html === '') return $html; + + // quick bail out if the page has no internal link markers + if (!str_contains($html, 'data-dw2pdf-target')) { + return $html; + } + + $pages = $collector->getPages(); + if ($pages === []) { + return $html; + } + $pages = array_fill_keys($pages, true); + + $dom = new DOMDocument('1.0', 'UTF-8'); + $previous = libxml_use_internal_errors(true); + $loaded = $dom->loadHTML( + '' . '
    ' . $html . '
    ', + LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD + ); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + + if (!$loaded) { + return $html; + } + + $anchors = $dom->getElementsByTagName('a'); + if ($anchors->length === 0) { + return $html; + } + + $pageAnchors = []; + foreach ($anchors as $anchor) { + /** @var \DOMElement $anchor */ + if (!$anchor->hasAttribute('data-dw2pdf-target')) { + continue; + } + + $target = $anchor->getAttribute('data-dw2pdf-target'); + if ($target === '' || !isset($pages[$target])) { + $anchor->removeAttribute('data-dw2pdf-target'); + $anchor->removeAttribute('data-dw2pdf-hash'); + continue; + } + + if (!isset($pageAnchors[$target])) { + $check = false; + $pageAnchors[$target] = sectionID($target, $check); + } + + $hash = $anchor->getAttribute('data-dw2pdf-hash'); + $anchor->setAttribute( + 'href', + '#' . $pageAnchors[$target] . '__' . $hash + ); + + $anchor->removeAttribute('data-dw2pdf-target'); + $anchor->removeAttribute('data-dw2pdf-hash'); + } + + $wrapper = $dom->getElementsByTagName('div')->item(0); + if (!$wrapper) { + return $html; + } + + $result = ''; + foreach ($wrapper->childNodes as $node) { + $result .= $dom->saveHTML($node); + } + + return $result; + } + + /** + * Write the Table of Contents + * + * For double-sided documents the ToC is always on an even number of pages, so that the + * following content is on the correct odd/even page. + * The first page of ToC starts always at an odd page, so an additional blank page might + * be included before. + * There is no page numbering at the pages of the ToC. + * + * @param string $header The header text for the ToC (localized)) + * @return void + * @throws MpdfException + */ + public function toc(string $header): void + { + $this->mpdf->TOCpagebreakByArray([ + 'toc-preHTML' => '

    ' . $header . '

    ', + 'toc-bookmarkText' => $header, + 'links' => true, + 'outdent' => '1em', + 'pagenumstyle' => '1' + ]); + + $this->write('', HTMLParserMode::HTML_BODY, false, false); + } + + /** + * Insert a cover page + * + * Should be called once at the beginning of the PDF generation. Will do nothing if + * no cover page is configured. + * + * @return void + * @throws MpdfException + */ + public function cover(): void + { + $html = $this->template->getHTML('cover'); + if (!$html) return; + + $this->conditionalPageBreak(); + $this->applyHeaderFooters(); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); + + $this->breakAfterMe(); + } + + /** + * Insert a back page + * + * Should be called once at the end of the PDF generation. Will do nothing if + * no back page is configured. + * + * @return void + * @throws MpdfException + */ + public function back(): void + { + $html = $this->template->getHTML('back'); + if (!$html) return; + + $this->conditionalPageBreak(); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); + } + + /** + * Finalize the document + * + * @return void + * @throws MpdfException + */ + public function endDocument(): void + { + // adds the closing div and finalizes the document + $this->write('
    ', HTMLParserMode::HTML_BODY, false, true); + } + + /** + * Set new headers and footers + * + * This will call the appropriate mpdf methods to set headers and footers. It should be called + * before each wiki page is added to the PDF. + * + * On first call on this instance it will set the headers/footers for the first page, afterwards + * it will use the standard headers/footers. + * + * We always set even and odd headers/footers, though they may be identical. + * @return void + */ + protected function applyHeaderFooters(): void + { + if ($this->isFirstPage) { + $header = $this->template->getHTML('header', 'first'); + $footer = $this->template->getHTML('footer', 'first'); + + if ($header) { + $this->mpdf->SetHTMLHeader($header, 'O'); + $this->mpdf->SetHTMLHeader($header, 'E'); + } + if ($footer) { + $this->mpdf->SetHTMLFooter($footer, 'O'); + $this->mpdf->SetHTMLFooter($footer, 'E'); + } + $this->isFirstPage = false; + } else { + $headerOdd = $this->template->getHTML('header', 'odd'); + $headerEven = $this->template->getHTML('header', 'even'); + $footerOdd = $this->template->getHTML('footer', 'odd'); + $footerEven = $this->template->getHTML('footer', 'even'); + + if ($headerOdd) { + $this->mpdf->SetHTMLHeader($headerOdd, 'O'); + } + if ($headerEven) { + $this->mpdf->SetHTMLHeader($headerEven, 'E'); + } + if ($footerOdd) { + $this->mpdf->SetHTMLFooter($footerOdd, 'O'); + } + if ($footerEven) { + $this->mpdf->SetHTMLFooter($footerEven, 'E'); + } + } + } + + /** + * Insert a page break if there was previous content + * + * @return void + * @throws MpdfException + */ + protected function conditionalPageBreak(): void + { + if ($this->breakBeforeNext) { + $this->pageBreak(); + $this->breakBeforeNext = false; + } + } + + /** + * Signal that a page break should be inserted before the next content + * + * @return void + */ + protected function breakAfterMe(): void + { + $this->breakBeforeNext = true; + } + + /** + * Return the debug HTML collected so far + * + * Will return an empty string if debugging is not enabled. + * + * @return string The collected debug HTML + */ + public function getDebugHTML(): string + { + return $this->debugHTML; + } + + /** + * Persist the generated PDF to the provided destination file. + * + * @param string $cacheFile Absolute file path that should receive the PDF output + * @return void + * @throws MpdfException + */ + public function outputToFile(string $cacheFile): void + { + $this->mpdf->Output($cacheFile, 'F'); + } + + /** + * A wrapper around MPDF::WriteHTML + * + * When debugging is enabled, the output is written to a debug buffer instead of the PDF. + * + * @param string $html The HTML code to write + * @param int $mode Use HTMLParserMode constants. Controls what parts of the $html code is parsed. + * @param bool $init Clears and sets buffers to Top level block etc. + * @param bool $close If false leaves buffers etc. in current state, so that it can continue a block etc. + * @throws MpdfException + */ + protected function write( + string $html, + int $mode = HTMLParserMode::DEFAULT_MODE, + bool $init = true, + bool $close = true + ) { + if (!$this->debug) { + try { + $this->mpdf->WriteHTML($html, $mode, $init, $close); + } catch (MpdfException $e) { + ErrorHandler::logException($e); // ensure the issue is logged + throw $e; + } + return; + } + + // when debugging, just store the HTML + if ($mode === HTMLParserMode::HEADER_CSS) { + $this->debugHTML .= "\n\n"; + } else { + $this->debugHTML .= "\n" . $html . "\n"; + } + } +} diff --git a/src/attributes/FromConfig.php b/src/attributes/FromConfig.php new file mode 100644 index 00000000..8392f966 --- /dev/null +++ b/src/attributes/FromConfig.php @@ -0,0 +1,19 @@ + + +Page Number: @PAGE@ +Total Pages: @PAGES@ +Document Title: @TITLE@ +Wiki Title: @WIKI@ +Wiki URL: @WIKIURL@ +Date: @DATE@ +User: @USERNAME@ +Base Path: @BASE@ +Include Dir: @INC@ +Template Base Path: @TPLBASE@ +Template Include Dir: @TPLINC@ +Page ID: @ID@ +Revision: @UPDATE@ +Page URL: @PAGEURL@ +QR Code: @QRCODE@ diff --git a/vendor/autoload.php b/vendor/autoload.php index 2b2076d9..14d9b31d 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,10 +14,7 @@ echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 51e734a7..2052022f 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -26,12 +26,23 @@ */ class InstalledVersions { + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; + /** + * @var bool + */ + private static $installedIsLocalDir; + /** * @var bool|null */ @@ -309,6 +320,24 @@ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; } /** @@ -322,19 +351,27 @@ private static function getInstalled() } $installed = array(); + $copiedLocalDir = false; if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; - if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { - self::$installed = $installed[count($installed) - 1]; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; } } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } } } @@ -350,7 +387,7 @@ private static function getInstalled() } } - if (self::$installed !== array()) { + if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 40ed7678..35034b68 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -7,4 +7,5 @@ return array( '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + 'db356362850385d08a5381de2638b5fd' => $vendorDir . '/mpdf/mpdf/src/functions.php', ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 9b0e6acc..1baeb5f1 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -8,7 +8,10 @@ return array( 'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'Mpdf\\QrCode\\' => array($vendorDir . '/mpdf/qrcode/src'), + 'Mpdf\\PsrLogAwareTrait\\' => array($vendorDir . '/mpdf/psr-log-aware-trait/src'), + 'Mpdf\\PsrHttpMessageShim\\' => array($vendorDir . '/mpdf/psr-http-message-shim/src'), 'Mpdf\\' => array($vendorDir . '/mpdf/mpdf/src'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index dc54eac1..d22f2282 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -8,6 +8,7 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04 { public static $files = array ( '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + 'db356362850385d08a5381de2638b5fd' => __DIR__ . '/..' . '/mpdf/mpdf/src/functions.php', ); public static $prefixLengthsPsr4 = array ( @@ -18,10 +19,13 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04 'P' => array ( 'Psr\\Log\\' => 8, + 'Psr\\Http\\Message\\' => 17, ), 'M' => array ( 'Mpdf\\QrCode\\' => 12, + 'Mpdf\\PsrLogAwareTrait\\' => 22, + 'Mpdf\\PsrHttpMessageShim\\' => 24, 'Mpdf\\' => 5, ), 'D' => @@ -39,10 +43,22 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04 array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + ), 'Mpdf\\QrCode\\' => array ( 0 => __DIR__ . '/..' . '/mpdf/qrcode/src', ), + 'Mpdf\\PsrLogAwareTrait\\' => + array ( + 0 => __DIR__ . '/..' . '/mpdf/psr-log-aware-trait/src', + ), + 'Mpdf\\PsrHttpMessageShim\\' => + array ( + 0 => __DIR__ . '/..' . '/mpdf/psr-http-message-shim/src', + ), 'Mpdf\\' => array ( 0 => __DIR__ . '/..' . '/mpdf/mpdf/src', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 12b00b9b..eb5ca37b 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,33 +2,36 @@ "packages": [ { "name": "mpdf/mpdf", - "version": "v8.0.17", - "version_normalized": "8.0.17.0", + "version": "v8.2.7", + "version_normalized": "8.2.7.0", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { "ext-gd": "*", "ext-mbstring": "*", + "mpdf/psr-http-message-shim": "^1.0 || ^2.0", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0", - "psr/log": "^1.0 || ^2.0", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" }, "require-dev": { "mockery/mockery": "^1.3.0", "mpdf/qrcode": "^1.1.0", "squizlabs/php_codesniffer": "^3.5.0", - "tracy/tracy": "^2.4", + "tracy/tracy": "~2.5", "yoast/phpunit-polyfills": "^1.0" }, "suggest": { @@ -36,10 +39,13 @@ "ext-xml": "Needed mainly for SVG manipulation", "ext-zlib": "Needed for compression of embedded resources, such as fonts" }, - "time": "2022-01-20T10:51:40+00:00", + "time": "2025-12-01T10:18:02+00:00", "type": "library", "installation-source": "dist", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { "Mpdf\\": "src/" } @@ -66,7 +72,7 @@ "utf-8" ], "support": { - "docs": "http://mpdf.github.io", + "docs": "https://mpdf.github.io", "issues": "https://github.com/mpdf/mpdf/issues", "source": "https://github.com/mpdf/mpdf" }, @@ -78,19 +84,117 @@ ], "install-path": "../mpdf/mpdf" }, + { + "name": "mpdf/psr-http-message-shim", + "version": "v2.0.1", + "version_normalized": "2.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-http-message-shim.git", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f", + "shasum": "" + }, + "require": { + "psr/http-message": "^2.0" + }, + "time": "2023-10-02T14:34:03+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Mpdf\\PsrHttpMessageShim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + }, + { + "name": "Nigel Cunningham", + "email": "nigel.cunningham@technocrat.com.au" + } + ], + "description": "Shim to allow support of different psr/message versions.", + "support": { + "issues": "https://github.com/mpdf/psr-http-message-shim/issues", + "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1" + }, + "install-path": "../mpdf/psr-http-message-shim" + }, + { + "name": "mpdf/psr-log-aware-trait", + "version": "v2.0.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0" + }, + "time": "2023-05-03T06:18:28+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ], + "description": "Trait to allow support of different psr/log versions.", + "support": { + "issues": "https://github.com/mpdf/psr-log-aware-trait/issues", + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0" + }, + "install-path": "../mpdf/psr-log-aware-trait" + }, { "name": "mpdf/qrcode", - "version": "v1.2.0", - "version_normalized": "1.2.0.0", + "version": "v1.2.1", + "version_normalized": "1.2.1.0", "source": { "type": "git", "url": "https://github.com/mpdf/qrcode.git", - "reference": "0c09fce8b28707611c3febdd1ca424d40f172184" + "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/qrcode/zipball/0c09fce8b28707611c3febdd1ca424d40f172184", - "reference": "0c09fce8b28707611c3febdd1ca424d40f172184", + "url": "https://api.github.com/repos/mpdf/qrcode/zipball/5320c512776aa3c199bd8be8f707ec83d9779d85", + "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85", "shasum": "" }, "require": { @@ -107,7 +211,7 @@ "ext-gd": "To output QR codes to PNG files", "ext-simplexml": "To output QR codes to SVG files" }, - "time": "2022-01-11T09:42:41+00:00", + "time": "2024-06-04T13:40:39+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -139,7 +243,7 @@ ], "support": { "issues": "https://github.com/mpdf/qrcode/issues", - "source": "https://github.com/mpdf/qrcode/tree/v1.2.0" + "source": "https://github.com/mpdf/qrcode/tree/v1.2.1" }, "funding": [ { @@ -151,40 +255,42 @@ }, { "name": "myclabs/deep-copy", - "version": "1.10.1", - "version_normalized": "1.10.1.0", + "version": "1.13.4", + "version_normalized": "1.13.4.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, - "time": "2020-06-29T13:22:24+00:00", + "time": "2025-08-01T08:46:24+00:00", "type": "library", "installation-source": "dist", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -198,27 +304,93 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], "install-path": "../myclabs/deep-copy" }, + { + "name": "psr/http-message", + "version": "2.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2023-04-04T09:54:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "install-path": "../psr/http-message" + }, { "name": "psr/log", - "version": "1.1.3", - "version_normalized": "1.1.3.0", + "version": "1.1.4", + "version_normalized": "1.1.4.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { "php": ">=5.3.0" }, - "time": "2020-03-23T09:12:05+00:00", + "time": "2021-05-03T11:20:27+00:00", "type": "library", "extra": { "branch-alias": { @@ -238,7 +410,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -248,40 +420,44 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "install-path": "../psr/log" }, { "name": "setasign/fpdi", - "version": "v2.3.3", - "version_normalized": "2.3.3.0", + "version": "v2.6.4", + "version_normalized": "2.6.4.0", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "50c388860a73191e010810ed57dbed795578e867" + "reference": "4b53852fde2734ec6a07e458a085db627c60eada" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/50c388860a73191e010810ed57dbed795578e867", - "reference": "50c388860a73191e010810ed57dbed795578e867", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^5.6 || ^7.0" + "php": "^7.1 || ^8.0" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "~5.7", - "setasign/fpdf": "~1.8", - "setasign/tfpdf": "1.31", - "tecnickcom/tcpdf": "~6.2" + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" }, "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." }, - "time": "2020-04-28T12:40:35+00:00", + "time": "2025-08-05T09:57:14+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -312,6 +488,16 @@ "fpdi", "pdf" ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], "install-path": "../setasign/fpdi" } ], diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 41dd26d4..d3053b11 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'splitbrain/dw2pdf', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '7d636181fc5bb007c30530a67e7c156f19ded673', + 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,34 +11,49 @@ ), 'versions' => array( 'mpdf/mpdf' => array( - 'pretty_version' => 'v8.0.17', - 'version' => '8.0.17.0', - 'reference' => '5f64118317c8145c0abc606b310aa0a66808398a', + 'pretty_version' => 'v8.2.7', + 'version' => '8.2.7.0', + 'reference' => 'b59670a09498689c33ce639bac8f5ba26721dab3', 'type' => 'library', 'install_path' => __DIR__ . '/../mpdf/mpdf', 'aliases' => array(), 'dev_requirement' => false, ), + 'mpdf/psr-http-message-shim' => array( + 'pretty_version' => 'v2.0.1', + 'version' => '2.0.1.0', + 'reference' => 'f25a0153d645e234f9db42e5433b16d9b113920f', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mpdf/psr-http-message-shim', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mpdf/psr-log-aware-trait' => array( + 'pretty_version' => 'v2.0.0', + 'version' => '2.0.0.0', + 'reference' => '7a077416e8f39eb626dee4246e0af99dd9ace275', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mpdf/psr-log-aware-trait', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'mpdf/qrcode' => array( - 'pretty_version' => 'v1.2.0', - 'version' => '1.2.0.0', - 'reference' => '0c09fce8b28707611c3febdd1ca424d40f172184', + 'pretty_version' => 'v1.2.1', + 'version' => '1.2.1.0', + 'reference' => '5320c512776aa3c199bd8be8f707ec83d9779d85', 'type' => 'library', 'install_path' => __DIR__ . '/../mpdf/qrcode', 'aliases' => array(), 'dev_requirement' => false, ), 'myclabs/deep-copy' => array( - 'pretty_version' => '1.10.1', - 'version' => '1.10.1.0', - 'reference' => '969b211f9a51aa1f6c01d1d2aef56d3bd91598e5', + 'pretty_version' => '1.13.4', + 'version' => '1.13.4.0', + 'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a', 'type' => 'library', 'install_path' => __DIR__ . '/../myclabs/deep-copy', 'aliases' => array(), 'dev_requirement' => false, - 'replaced' => array( - 0 => '1.10.1', - ), ), 'paragonie/random_compat' => array( 'dev_requirement' => false, @@ -46,19 +61,28 @@ 0 => '*', ), ), + 'psr/http-message' => array( + 'pretty_version' => '2.0', + 'version' => '2.0.0.0', + 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-message', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'psr/log' => array( - 'pretty_version' => '1.1.3', - 'version' => '1.1.3.0', - 'reference' => '0f73288fd15629204f9d42b7055f72dacbe811fc', + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => false, ), 'setasign/fpdi' => array( - 'pretty_version' => 'v2.3.3', - 'version' => '2.3.3.0', - 'reference' => '50c388860a73191e010810ed57dbed795578e867', + 'pretty_version' => 'v2.6.4', + 'version' => '2.6.4.0', + 'reference' => '4b53852fde2734ec6a07e458a085db627c60eada', 'type' => 'library', 'install_path' => __DIR__ . '/../setasign/fpdi', 'aliases' => array(), @@ -67,7 +91,7 @@ 'splitbrain/dw2pdf' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '7d636181fc5bb007c30530a67e7c156f19ded673', + 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 6d3407db..6cd6b536 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 70100)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 70200)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { @@ -19,8 +19,7 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml new file mode 100644 index 00000000..a88906a2 --- /dev/null +++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -0,0 +1,35 @@ +name: Bug report 🐛 +description: The library does not work as expected +body: + + - type: checkboxes + attributes: + label: Guidelines + description: Please confirm this is a bug report and not general troubleshooting. + options: + - label: I understand that [if I fail to adhere to contribution guidelines and/or fail to provide all required details, this issue may be closed without review](https://github.com/mpdf/mpdf/blob/development/.github/CONTRIBUTING.md). + required: true + + - type: textarea + attributes: + label: Description of the bug + validations: + required: true + + - type: input + attributes: + label: mPDF version + validations: + required: true + + - type: input + attributes: + label: PHP Version and environment (server type, cli provider etc., enclosing libraries and their respective versions) + validations: + required: true + + - type: textarea + attributes: + label: Reproducible PHP+CSS+HTML snippet suffering by the error + validations: + required: true diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml new file mode 100644 index 00000000..88d9bdca --- /dev/null +++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml @@ -0,0 +1,8 @@ +name: Feature request 🚀 +description: I would like to have a new functionality added +body: + - type: textarea + attributes: + label: Please describe the new functionality as best as you can. + validations: + required: true \ No newline at end of file diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index cbf80c85..00000000 --- a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report 🐛 -about: The library does not work as expected (please use not for troubleshooting) ---- - - - -### I found this bug - -### This is mPDF and PHP version and environment (server/fpm/cli etc) I am using - -### This is the PHP code snippet I use - -``` - diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml index 901d9c25..3c03ea0d 100644 --- a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml +++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: General questions and troubleshooting ❓ url: https://github.com/mpdf/mpdf/discussions diff --git a/vendor/mpdf/mpdf/.github/SECURITY.md b/vendor/mpdf/mpdf/.github/SECURITY.md new file mode 100644 index 00000000..71878d08 --- /dev/null +++ b/vendor/mpdf/mpdf/.github/SECURITY.md @@ -0,0 +1,5 @@ +How to disclose potential security issues +============ + +As mPDF does not have a domain or a dedicated contact apart from its Github repository, to prevent +disclosing maintainers' contacts publicly, please use [GitHub's Security Advisories system](https://github.com/mpdf/mpdf/security/advisories). \ No newline at end of file diff --git a/vendor/mpdf/mpdf/.github/workflows/coverage.yml b/vendor/mpdf/mpdf/.github/workflows/coverage.yml index 13b1207b..9ae56589 100644 --- a/vendor/mpdf/mpdf/.github/workflows/coverage.yml +++ b/vendor/mpdf/mpdf/.github/workflows/coverage.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/vendor/mpdf/mpdf/.github/workflows/cs.yml b/vendor/mpdf/mpdf/.github/workflows/cs.yml index ff003e01..c2a46d08 100644 --- a/vendor/mpdf/mpdf/.github/workflows/cs.yml +++ b/vendor/mpdf/mpdf/.github/workflows/cs.yml @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml b/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..3f97c7c9 --- /dev/null +++ b/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml @@ -0,0 +1,44 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Static Analysis check" + +on: + pull_request: + push: + branches: + - "master" + - "development" + - "test" + +jobs: + + stan: + + name: "Static Analysis check" + + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + php-version: + - "8.2" + + operating-system: [ubuntu-latest] + + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: "mbstring" + tools: composer:v2 + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress && composer require \"phpstan/phpstan:^2.0\"" + + - name: "Static Analysis check" + run: vendor/bin/phpstan --no-progress diff --git a/vendor/mpdf/mpdf/.github/workflows/tests.yml b/vendor/mpdf/mpdf/.github/workflows/tests.yml index ff64a816..a7dd7f45 100644 --- a/vendor/mpdf/mpdf/.github/workflows/tests.yml +++ b/vendor/mpdf/mpdf/.github/workflows/tests.yml @@ -1,6 +1,6 @@ # https://help.github.com/en/categories/automating-your-workflow-with-github-actions -name: "CI" +name: "Tests" on: pull_request: @@ -30,11 +30,15 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" operating-system: [ubuntu-latest, windows-latest] steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/vendor/mpdf/mpdf/.gitignore b/vendor/mpdf/mpdf/.gitignore index 0bfe817f..60278b15 100644 --- a/vendor/mpdf/mpdf/.gitignore +++ b/vendor/mpdf/mpdf/.gitignore @@ -1,2 +1,3 @@ vendor/* +tests/Mpdf/tmp/* composer.lock diff --git a/vendor/mpdf/mpdf/CHANGELOG.md b/vendor/mpdf/mpdf/CHANGELOG.md index 9c184e29..4c7b13fd 100644 --- a/vendor/mpdf/mpdf/CHANGELOG.md +++ b/vendor/mpdf/mpdf/CHANGELOG.md @@ -1,3 +1,44 @@ +mPDF 8.2.x +=========================== + +New features +------------ +* Watermark text can now be colored using \Mpdf\Watermark DTO. \Mpdf\WatermarkImage DTO for images. (#1876) +* Added support for `psr/http-message` v2 without dropping v1. (@markdorison, @apotek, @greg-1-anderson, @NigelCunningham #1907) +* PHP 8.3 support in mPDF 8.2.1 +* Add support for `page-break-before: avoid;` and `page-break-after: avoid;` for tr elements inside tables + +Bugfixes +-------- + +* Replace character entities with characters when processing the `code` attribute in the `` tag +* Escape XML predefined entities in XMP metadata (Fix for #2090) +* Enable Font Subsetting by Default (Fix for #1315) + +mPDF 8.1.x +=========================== + +New features +------------ + +* Service container for internal services +* Set /Lang entry for better accessibility when document language is available (@cuongmits, #1418) +* More verbose helper methods for `Output`: `OutputBinaryData`, `OutputHttpInline`, `OutputHttpDownload`, `OutputFile` (since v8.1.2) +* Set font-size to `auto` in textarea and input in active forms to resize the font-size (@ChrisB9, #1721) +* PHP 8.2 support in mPDF 8.1.3 +* Added support for `psr/log` v3 without dropping v2. (@markdorison, @apotek, @greg-1-anderson, #1857) + +Bugfixes +-------- + +* Better exception message about fonts with MarkGlyphSets (Fix for #1408) +* Updated Garuda font with fixed "k" character (Fix for #1440) +* Testing and suppressing PNG file conversion errors +* Prevent hyphenation of urls starting with https and e-mail addresses (@HKandulla, #1634) +* Colorspace restrictor reads mode from Mpdf and works again (Fix for #1094) +* Prevent exception when multiple columns wrap to next page +* Update default `curlUserAgent` configuration variable from Firefox 13 to 108 + mPDF 8.0.x =========================== @@ -37,6 +78,7 @@ mPDF 8.0.x * Fix: Using mpdf in phar package leads to weird errors (#1504, @sandreas) * WEBP images support (#1525) + mPDF 8.0.0 =========================== diff --git a/vendor/mpdf/mpdf/README.md b/vendor/mpdf/mpdf/README.md index 366ddace..c58e6660 100644 --- a/vendor/mpdf/mpdf/README.md +++ b/vendor/mpdf/mpdf/README.md @@ -18,11 +18,15 @@ Requirements PHP versions and extensions --------------------------- -- `mPDF >=7.0` is supported on `PHP ^5.6 || ~7.0.0 || ~7.1.0 || ~7.2.0` +- `PHP >=5.6 <7.3.0` is supported for `mPDF >= 7.0` - `PHP 7.3` is supported since `mPDF v7.1.7` - `PHP 7.4` is supported since `mPDF v8.0.4` - `PHP 8.0` is supported since `mPDF v8.0.10` - `PHP 8.1` is supported as of `mPDF v8.0.13` +- `PHP 8.2` is supported as of `mPDF v8.1.3` +- `PHP 8.3` is supported as of `mPDF v8.2.1` +- `PHP 8.4` is supported as of `mPDF v8.2.5` +- `PHP 8.5` is supported as of `mPDF v8.2.6` PHP `mbstring` and `gd` extensions have to be loaded. diff --git a/vendor/mpdf/mpdf/composer.json b/vendor/mpdf/mpdf/composer.json index c93e87d2..a8481f86 100644 --- a/vendor/mpdf/mpdf/composer.json +++ b/vendor/mpdf/mpdf/composer.json @@ -18,22 +18,25 @@ "support": { "issues": "https://github.com/mpdf/mpdf/issues", "source": "https://github.com/mpdf/mpdf", - "docs": "http://mpdf.github.io" + "docs": "https://mpdf.github.io" }, "require": { - "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "ext-gd": "*", "ext-mbstring": "*", + "mpdf/psr-http-message-shim": "^1.0 || ^2.0", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "psr/log": "^1.0 || ^2.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" }, "require-dev": { "mockery/mockery": "^1.3.0", "mpdf/qrcode": "^1.1.0", "squizlabs/php_codesniffer": "^3.5.0", - "tracy/tracy": "^2.4", + "tracy/tracy": "~2.5", "yoast/phpunit-polyfills": "^1.0" }, "suggest": { @@ -44,7 +47,10 @@ "autoload": { "psr-4": { "Mpdf\\": "src/" - } + }, + "files": [ + "src/functions.php" + ] }, "autoload-dev": { "psr-4": { @@ -58,7 +64,7 @@ "post-install-cmd": [ "php -r \"chmod('./tmp', 0777);\"" ], - "cs": "@php vendor/bin/phpcs -v --report-width=160 --standard=ruleset.xml --severity=1 --warning-severity=0 --extensions=php src utils tests", + "cs": "@php vendor/bin/phpcs --report-width=160 --standard=ruleset.xml --severity=1 --warning-severity=0 --extensions=php src utils tests", "test": "@php vendor/bin/phpunit", "coverage": "@php vendor/bin/phpunit --coverage-text" }, diff --git a/vendor/mpdf/mpdf/phpstan-baseline.neon b/vendor/mpdf/mpdf/phpstan-baseline.neon new file mode 100644 index 00000000..eed44e96 --- /dev/null +++ b/vendor/mpdf/mpdf/phpstan-baseline.neon @@ -0,0 +1,1459 @@ +parameters: + ignoreErrors: + - + message: '#^Method Mpdf\\Barcode\\Code39\:\:init\(\) should return array\ but return statement is missing\.$#' + identifier: return.missing + count: 1 + path: src/Barcode/Code39.php + + - + message: '#^Binary operation "%%" between string and 4 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Barcode/EanExt.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and non\-empty\-string results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Barcode/EanExt.php + + - + message: '#^Variable \$upceCode in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Barcode/EanUpc.php + + - + message: '#^Variable \$ret might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Color/ColorConverter.php + + - + message: '#^PHPDoc tag @param references unknown parameter\: \$mode$#' + identifier: parameter.notFound + count: 1 + path: src/Color/ColorSpaceRestrictor.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/CssManager.php + + - + message: '#^Variable \$tag might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/CssManager.php + + - + message: '#^Method Mpdf\\Mpdf\:\:GetJspacing\(\) invoked with 4 parameters, 5 required\.$#' + identifier: arguments.count + count: 2 + path: src/DirectWrite.php + + - + message: '#^Undefined variable\: \$false$#' + identifier: variable.undefined + count: 1 + path: src/DirectWrite.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Form.php + + - + message: '#^Variable \$js in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 2 + path: src/Form.php + + - + message: '#^Variable \$radio_background_color might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Form.php + + - + message: '#^Variable \$radio_color might not be defined\.$#' + identifier: variable.undefined + count: 10 + path: src/Form.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Gradient.php + + - + message: '#^Variable \$angle in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Gradient.php + + - + message: '#^Variable \$repeat might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Gradient.php + + - + message: '#^Variable \$startx in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Gradient.php + + - + message: '#^Variable \$starty in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Gradient.php + + - + message: '#^Call to method getHost\(\) on an unknown class Mpdf\\Http\\Uri\.$#' + identifier: class.notFound + count: 2 + path: src/Http/SocketHttpClient.php + + - + message: '#^Call to method getPath\(\) on an unknown class Mpdf\\Http\\Uri\.$#' + identifier: class.notFound + count: 1 + path: src/Http/SocketHttpClient.php + + - + message: '#^Call to method getPort\(\) on an unknown class Mpdf\\Http\\Uri\.$#' + identifier: class.notFound + count: 2 + path: src/Http/SocketHttpClient.php + + - + message: '#^Call to method getQuery\(\) on an unknown class Mpdf\\Http\\Uri\.$#' + identifier: class.notFound + count: 1 + path: src/Http/SocketHttpClient.php + + - + message: '#^Call to method getScheme\(\) on an unknown class Mpdf\\Http\\Uri\.$#' + identifier: class.notFound + count: 1 + path: src/Http/SocketHttpClient.php + + - + message: '#^Instantiated class Mpdf\\Http\\Uri not found\.$#' + identifier: class.notFound + count: 1 + path: src/Http/SocketHttpClient.php + + - + message: '#^Variable \$c might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/Bmp.php + + - + message: '#^Variable \$str might not be defined\.$#' + identifier: variable.undefined + count: 12 + path: src/Image/Bmp.php + + - + message: '#^Variable \$bgColor in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 2 + path: src/Image/ImageProcessor.php + + - + message: '#^Variable \$info might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/ImageProcessor.php + + - + message: '#^Variable \$ncols might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Image/ImageProcessor.php + + - + message: '#^Binary operation "\*" between 0\.3333333333333333 and string results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Image/Svg.php + + - + message: '#^Binary operation "\*" between string and 2 results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Image/Svg.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and \(float\|int\) results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Image/Svg.php + + - + message: '#^Binary operation "\+" between string and \(float\|int\) results in an error\.$#' + identifier: binaryOp.invalid + count: 3 + path: src/Image/Svg.php + + - + message: '#^Binary operation "\+\=" between 0\|string and non\-empty\-string results in an error\.$#' + identifier: assignOp.invalid + count: 4 + path: src/Image/Svg.php + + - + message: '#^Binary operation "\-" between string and string results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Image/Svg.php + + - + message: '#^PHPDoc tag @var has invalid value \(\$styleNode \\DOMNode\)\: Unexpected token "\$styleNode", expected type at offset 9 on line 1$#' + identifier: phpDoc.parseError + count: 1 + path: src/Image/Svg.php + + - + message: '#^Unary operation "\-" on non\-empty\-string results in an error\.$#' + identifier: unaryOp.invalid + count: 2 + path: src/Image/Svg.php + + - + message: '#^Unary operation "\-" on string results in an error\.$#' + identifier: unaryOp.invalid + count: 50 + path: src/Image/Svg.php + + - + message: '#^Variable \$c in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Image/Svg.php + + - + message: '#^Variable \$class in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: src/Image/Svg.php + + - + message: '#^Variable \$color_final might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/Svg.php + + - + message: '#^Variable \$inners might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/Svg.php + + - + message: '#^Variable \$path_style might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/Svg.php + + - + message: '#^Variable \$op might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Image/Wmf.php + + - + message: '#^Variable \$parms might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: src/Image/Wmf.php + + - + message: '#^Binary operation "\*" between string and 100 results in an error\.$#' + identifier: binaryOp.invalid + count: 4 + path: src/Mpdf.php + + - + message: '#^Binary operation "\*" between string and 2 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Mpdf.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Mpdf.php + + - + message: '#^Binary operation "\+" between string and \(float\|int\) results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Mpdf.php + + - + message: '#^Binary operation "\-" between int\<0, max\> and string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Mpdf.php + + - + message: '#^Binary operation "\-" between string and \(float\|int\) results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Mpdf.php + + - + message: '#^Call to function unset\(\) contains undefined variable \$cell\.$#' + identifier: unset.variable + count: 1 + path: src/Mpdf.php + + - + message: '#^Call to function unset\(\) contains undefined variable \$content\.$#' + identifier: unset.variable + count: 1 + path: src/Mpdf.php + + - + message: '#^Call to sprintf contains 0 placeholders, 1 value given\.$#' + identifier: argument.sprintf + count: 1 + path: src/Mpdf.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Mpdf.php + + - + message: '#^PHPDoc tag @throws with type Mpdf\\FilterException\|Mpdf\\PdfParserException\|Mpdf\\PdfReaderException\|setasign\\Fpdi\\PdfParser\\CrossReference\\CrossReferenceException\|setasign\\Fpdi\\PdfParser\\Type\\PdfTypeException is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 1 + path: src/Mpdf.php + + - + message: '#^PHPDoc tag @var has invalid value \(\$value PdfIndirectObject\)\: Unexpected token "\$value", expected type at offset 15 on line 2$#' + identifier: phpDoc.parseError + count: 2 + path: src/Mpdf.php + + - + message: '#^PHPDoc tag @var has invalid value \(\$value PdfIndirectObject\)\: Unexpected token "\$value", expected type at offset 16 on line 2$#' + identifier: phpDoc.parseError + count: 1 + path: src/Mpdf.php + + - + message: '#^Undefined variable\: \$bcor$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Undefined variable\: \$t_tod$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Undefined variable\: \$xadj$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$aarr might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$adjx might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$adjy might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$adv might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$b2 might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$bb might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_bb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_bl might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_br might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_bt might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_pb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_pl might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_pr might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_pt might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_x might not be defined\.$#' + identifier: variable.undefined + count: 10 + path: src/Mpdf.php + + - + message: '#^Variable \$bbox_y might not be defined\.$#' + identifier: variable.undefined + count: 10 + path: src/Mpdf.php + + - + message: '#^Variable \$blm might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$blw might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$blx might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$bodystyle in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$cbord might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$cell in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$charLI might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$charLO might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$charRI might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$chars might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$charspacing might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$codestr_fontsize might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/Mpdf.php + + - + message: '#^Variable \$cw might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$desc might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$ep_present might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$ep_reset might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$ep_suppress might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$ep_type might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$extraHeight might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$extraWidth might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$fx might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$glyphYorigin might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/Mpdf.php + + - + message: '#^Variable \$h might not be defined\.$#' + identifier: variable.undefined + count: 12 + path: src/Mpdf.php + + - + message: '#^Variable \$info might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$inner_h might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$inner_w might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$innerp might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$leveladj might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$llm might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$lx1 might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$lx2 might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$ly1 might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$ly2 might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$newarr might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$notfullwidth might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$outerfilled might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$outerfontsize might not be defined\.$#' + identifier: variable.undefined + count: 8 + path: src/Mpdf.php + + - + message: '#^Variable \$outerp might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$p might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$preroth might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: src/Mpdf.php + + - + message: '#^Variable \$prerotw might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: src/Mpdf.php + + - + message: '#^Variable \$ratio might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$rlm might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$rot_bpos might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$rot_rpos might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Mpdf.php + + - + message: '#^Variable \$sarr might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$save_fill might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Variable \$sp_present might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$sp_reset might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$sp_suppress might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$sp_type might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$t might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$texto might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: src/Mpdf.php + + - + message: '#^Variable \$textw might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$thuborddet might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$tisbnm might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$tlm might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$tntborddet might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/Mpdf.php + + - + message: '#^Variable \$tp_present might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$tp_reset might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$tp_suppress might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$tp_type might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$up might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$ut might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Mpdf.php + + - + message: '#^Variable \$va in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$w might not be defined\.$#' + identifier: variable.undefined + count: 12 + path: src/Mpdf.php + + - + message: '#^Variable \$x0 might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Mpdf.php + + - + message: '#^Variable \$zarr might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Mpdf.php + + - + message: '#^Array has 2 duplicate keys with value ''rphf'' \(''rphf'', ''rphf''\)\.$#' + identifier: array.duplicateKey + count: 1 + path: src/Otl.php + + - + message: '#^Offset int on array\{\} in isset\(\) does not exist\.$#' + identifier: isset.offset + count: 2 + path: src/Otl.php + + - + message: '#^Offset int\<0, max\> on array\{\} in isset\(\) does not exist\.$#' + identifier: isset.offset + count: 4 + path: src/Otl.php + + - + message: '#^Offset int\<1, max\> on array\{\} in isset\(\) does not exist\.$#' + identifier: isset.offset + count: 2 + path: src/Otl.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Otl.php + + - + message: '#^Variable \$Backtrack might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$ChainSubRule might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$CoverageBacktrackOffset might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Otl.php + + - + message: '#^Variable \$CoverageInputOffset might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Otl.php + + - + message: '#^Variable \$CoverageLookaheadOffset might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Otl.php + + - + message: '#^Variable \$GlyphID might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$Input might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$Lookahead might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$LookupListIndex might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Otl.php + + - + message: '#^Variable \$PosLookupRecord might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$SequenceIndex might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Otl.php + + - + message: '#^Variable \$SubstLookupRecord might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Otl.php + + - + message: '#^Variable \$Value might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$backtrackGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$cctr might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Otl.php + + - + message: '#^Variable \$ic might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$lookaheadGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$newOTLdata might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$next_level might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Variable \$shift might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: src/Otl.php + + - + message: '#^Variable \$useGSUBtags might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Otl.php + + - + message: '#^Undefined variable\: \$rtlPUAarr$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Undefined variable\: \$rtlPUAstr$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$GSLookup might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$GSUBScriptLang might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$MarkGlyphSetsDef_offset might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$arr might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$backtrackGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/OtlDump.php + + - + message: '#^Variable \$gsub might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/OtlDump.php + + - + message: '#^Variable \$lookaheadGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/OtlDump.php + + - + message: '#^Variable \$subRule might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/OtlDump.php + + - + message: '#^Variable \$type might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/OtlDump.php + + - + message: '#^Variable \$new_reph_pos might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Shaper/Indic.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/TTFontFile.php + + - + message: '#^Undefined variable\: \$fsSelection$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Undefined variable\: \$post$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$MarkGlyphSetsDef_offset might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$arr might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$backtrackGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: src/TTFontFile.php + + - + message: '#^Variable \$format might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$glyphData might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/TTFontFile.php + + - + message: '#^Variable \$head_start might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$kk might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/TTFontFile.php + + - + message: '#^Variable \$locaH might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$locax might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TTFontFile.php + + - + message: '#^Variable \$lookaheadGlyphs might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/TTFontFile.php + + - + message: '#^Variable \$type might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/TTFontFile.php + + - + message: '#^Variable \$up might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/TTFontFile.php + + - + message: '#^Binary operation "&" between '''' and 1 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/TTFontFileAnalysis.php + + - + message: '#^Binary operation "&" between '''' and 32 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/TTFontFileAnalysis.php + + - + message: '#^Variable \$TOC_end might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TableOfContents.php + + - + message: '#^Variable \$TOC_npages might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TableOfContents.php + + - + message: '#^Variable \$TOC_start might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/TableOfContents.php + + - + message: '#^Method Mpdf\\Tag\:\:getTagInstance\(\) should return Mpdf\\Tag\\Tag but return statement is missing\.$#' + identifier: return.missing + count: 1 + path: src/Tag.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Tag.php + + - + message: '#^Variable \$blockstate might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Tag/BlockTag.php + + - + message: '#^Variable \$added_page might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/Tag/Table.php + + - + message: '#^Variable \$blockstate might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Tag/Table.php + + - + message: '#^Variable \$fullpage might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Tag/Table.php + + - + message: '#^Variable \$maxfirstrowheight might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Tag/Table.php + + - + message: '#^Variable \$maxrowheight might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Tag/Table.php + + - + message: '#^Variable \$remainingpage might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: src/Tag/Table.php + + - + message: '#^Variable \$save_table might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Tag/Table.php + + - + message: '#^Variable \$tableheight might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/Tag/Table.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Tag/Tag.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$f1 might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$f2 might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$fo_h might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$fo_w might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$img might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$wmf_x might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Variable \$wmf_y might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/BackgroundWriter.php + + - + message: '#^Binary operation "/" between non\-falsy\-string and 2\.834645669291339 results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: src/Writer/BaseWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/BaseWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/BookmarkWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/ColorWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/FontWriter.php + + - + message: '#^Variable \$codeToGlyph might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/FontWriter.php + + - + message: '#^Variable \$fontstream might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/Writer/FontWriter.php + + - + message: '#^Variable \$ttfontsize might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Writer/FontWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/FormWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/ImageWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/JavaScriptWriter.php + + - + message: '#^Binary operation "\*" between 2 and string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Writer/MetadataWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/MetadataWriter.php + + - + message: '#^Access to constant TYPE_STREAM on an unknown class pdf_parser\.$#' + identifier: class.notFound + count: 1 + path: src/Writer/ObjectWriter.php + + - + message: '#^Call to an undefined method Mpdf\\Mpdf\:\:pdf_write_value\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Writer/ObjectWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/ObjectWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/OptionalContentWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/PageWriter.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#' + identifier: phpDoc.parseError + count: 2 + path: src/Writer/ResourceWriter.php diff --git a/vendor/mpdf/mpdf/phpstan.neon b/vendor/mpdf/mpdf/phpstan.neon new file mode 100644 index 00000000..ad4fa5de --- /dev/null +++ b/vendor/mpdf/mpdf/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 2 + paths: + - src + treatPhpDocTypesAsCertain: false + +includes: + - phpstan-baseline.neon \ No newline at end of file diff --git a/vendor/mpdf/mpdf/src/AssetFetcher.php b/vendor/mpdf/mpdf/src/AssetFetcher.php new file mode 100644 index 00000000..267ee996 --- /dev/null +++ b/vendor/mpdf/mpdf/src/AssetFetcher.php @@ -0,0 +1,120 @@ +mpdf = $mpdf; + $this->contentLoader = $contentLoader; + $this->http = $http; + $this->logger = $logger; + } + + public function fetchDataFromPath($path, $originalSrc = null) + { + /** + * Prevents insecure PHP object injection through phar:// wrapper + * @see https://github.com/mpdf/mpdf/issues/949 + * @see https://github.com/mpdf/mpdf/issues/1381 + */ + $wrapperChecker = new StreamWrapperChecker($this->mpdf); + + if ($wrapperChecker->hasBlacklistedStreamWrapper($path)) { + throw new \Mpdf\Exception\AssetFetchingException('File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); + } + + if ($originalSrc && $wrapperChecker->hasBlacklistedStreamWrapper($originalSrc)) { + throw new \Mpdf\Exception\AssetFetchingException('File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); + } + + $this->mpdf->GetFullPath($path); + + return $this->isPathLocal($path) || ($originalSrc !== null && $this->isPathLocal($originalSrc)) + ? $this->fetchLocalContent($path, $originalSrc) + : $this->fetchRemoteContent($path); + } + + public function fetchLocalContent($path, $originalSrc) + { + $data = ''; + + if ($originalSrc && $this->mpdf->basepathIsLocal && $check = @fopen($originalSrc, 'rb')) { + fclose($check); + $path = $originalSrc; + $this->logger->debug(sprintf('Fetching content of file "%s" with local basepath', $path), ['context' => LogContext::REMOTE_CONTENT]); + + return $this->contentLoader->load($path); + } + + if ($path && $check = @fopen($path, 'rb')) { + fclose($check); + $this->logger->debug(sprintf('Fetching content of file "%s" with non-local basepath', $path), ['context' => LogContext::REMOTE_CONTENT]); + + return $this->contentLoader->load($path); + } + + return $data; + } + + public function fetchRemoteContent($path) + { + $data = ''; + + try { + + $this->logger->debug(sprintf('Fetching remote content of file "%s"', $path), ['context' => LogContext::REMOTE_CONTENT]); + + /** @var \Mpdf\PsrHttpMessageShim\Response $response */ + $response = $this->http->sendRequest(new Request('GET', $path)); + + if (!str_starts_with((string) $response->getStatusCode(), '2')) { + + $message = sprintf('Non-OK HTTP response "%s" on fetching remote content "%s" because of an error', $response->getStatusCode(), $path); + if ($this->mpdf->debug) { + throw new \Mpdf\MpdfException($message); + } + + $this->logger->info($message); + + return $data; + } + + $data = $response->getBody()->getContents(); + + } catch (\InvalidArgumentException $e) { + $message = sprintf('Unable to fetch remote content "%s" because of an error "%s"', $path, $e->getMessage()); + if ($this->mpdf->debug) { + throw new \Mpdf\MpdfException($message, 0, E_ERROR, null, null, $e); + } + + $this->logger->warning($message); + } + + return $data; + } + + public function isPathLocal($path) + { + return str_starts_with($path, 'file://') || strpos($path, '://') === false; // @todo More robust implementation + } + +} diff --git a/vendor/mpdf/mpdf/src/Cache.php b/vendor/mpdf/mpdf/src/Cache.php index 5554575e..5161145f 100644 --- a/vendor/mpdf/mpdf/src/Cache.php +++ b/vendor/mpdf/mpdf/src/Cache.php @@ -28,10 +28,6 @@ public function __construct($basePath, $cleanupInterval = 3600) protected function createBasePath($basePath) { if (!file_exists($basePath)) { - if (!$this->createBasePath(dirname($basePath))) { - return false; - } - if (!$this->createDirectory($basePath)) { return false; } @@ -46,15 +42,33 @@ protected function createBasePath($basePath) protected function createDirectory($basePath) { - if (!mkdir($basePath)) { + $permissions = $this->getPermission($this->getExistingParentDirectory($basePath)); + if (! mkdir($basePath, $permissions, true)) { return false; } - if (!chmod($basePath, 0777)) { - return false; + return true; + } + + protected function getExistingParentDirectory($basePath) + { + $targetParent = dirname($basePath); + while ($targetParent !== '.' && ! is_dir($targetParent) && dirname($targetParent) !== $targetParent) { + $targetParent = dirname($targetParent); } - return true; + return realpath($targetParent); + } + + protected function getPermission($basePath, $fallbackPermission = 0777) + { + if (! is_dir($basePath)) { + return $fallbackPermission; + } + + $result = fileperms($basePath); + + return $result ? $result & 0007777 : $fallbackPermission; } public function tempFilename($filename) diff --git a/vendor/mpdf/mpdf/src/Color/ColorConverter.php b/vendor/mpdf/mpdf/src/Color/ColorConverter.php index fe5e547d..9cb59ea4 100644 --- a/vendor/mpdf/mpdf/src/Color/ColorConverter.php +++ b/vendor/mpdf/mpdf/src/Color/ColorConverter.php @@ -193,6 +193,10 @@ private function convertPlain($color, array &$PDFAXwarnings = []) } elseif (strpos($color, '#') === 0) { // case of #nnnnnn or #nnn $c = $this->processHashColor($color); } elseif (preg_match('/(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\((.*?)\)/', $color, $m)) { + // ignore colors containing CSS variables + if (str_starts_with(mb_strtolower($m[2]), 'var(--')) { + $m[2] = '0, 0, 0, 100'; + } $c = $this->processModeColor($m[1], explode(',', $m[2])); } diff --git a/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php b/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php index 195138f5..aeed5dbb 100644 --- a/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php +++ b/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php @@ -38,11 +38,10 @@ class ColorSpaceRestrictor * @param \Mpdf\Color\ColorModeConverter $colorModeConverter * @param int $mode */ - public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter, $mode) + public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter) { $this->mpdf = $mpdf; $this->colorModeConverter = $colorModeConverter; - $this->mode = $mode; } /** @@ -93,11 +92,11 @@ private function restrictSpotColorSpace($c, &$PDFAXwarnings = []) if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) { $PDFAXwarnings[] = "Spot color specified '" . $this->mpdf->spotColorIDs[$c[1]] . "' (converted to process color)"; } - if ($this->mode != 3) { + if ($this->mpdf->restrictColorSpace != 3) { $sp = $this->mpdf->spotColors[$this->mpdf->spotColorIDs[$c[1]]]; $c = $this->colorModeConverter->cmyk2rgb([4, $sp['c'], $sp['m'], $sp['y'], $sp['k']]); } - } elseif ($this->mode == 1) { + } elseif ($this->mpdf->restrictColorSpace == 1) { $sp = $this->mpdf->spotColors[$this->mpdf->spotColorIDs[$c[1]]]; $c = $this->colorModeConverter->cmyk2gray([4, $sp['c'], $sp['m'], $sp['y'], $sp['k']]); } @@ -114,14 +113,14 @@ private function restrictSpotColorSpace($c, &$PDFAXwarnings = []) */ private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = []) { - if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) { + if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) { if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { $PDFAXwarnings[] = "RGB color specified '" . $color . "' (converted to CMYK)"; } $c = $this->colorModeConverter->rgb2cmyk($c); - } elseif ($this->mode == 1) { + } elseif ($this->mpdf->restrictColorSpace == 1) { $c = $this->colorModeConverter->rgb2gray($c); - } elseif ($this->mode == 3) { + } elseif ($this->mpdf->restrictColorSpace == 3) { $c = $this->colorModeConverter->rgb2cmyk($c); } @@ -137,14 +136,14 @@ private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = []) */ private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = []) { - if ($this->mpdf->PDFA && $this->mode != 3) { + if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) { if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) { $PDFAXwarnings[] = "CMYK color specified '" . $color . "' (converted to RGB)"; } $c = $this->colorModeConverter->cmyk2rgb($c); - } elseif ($this->mode == 1) { + } elseif ($this->mpdf->restrictColorSpace == 1) { $c = $this->colorModeConverter->cmyk2gray($c); - } elseif ($this->mode == 2) { + } elseif ($this->mpdf->restrictColorSpace == 2) { $c = $this->colorModeConverter->cmyk2rgb($c); } @@ -160,21 +159,21 @@ private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = []) */ private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = []) { - if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) { + if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) { if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { $PDFAXwarnings[] = "RGB color with transparency specified '" . $color . "' (converted to CMYK without transparency)"; } $c = $this->colorModeConverter->rgb2cmyk($c); $c = [4, $c[1], $c[2], $c[3], $c[4]]; - } elseif ($this->mpdf->PDFA && $this->mode != 3) { + } elseif ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) { if (!$this->mpdf->PDFAauto) { $PDFAXwarnings[] = "RGB color with transparency specified '" . $color . "' (converted to RGB without transparency)"; } $c = $this->colorModeConverter->rgb2cmyk($c); $c = [4, $c[1], $c[2], $c[3], $c[4]]; - } elseif ($this->mode == 1) { + } elseif ($this->mpdf->restrictColorSpace == 1) { $c = $this->colorModeConverter->rgb2gray($c); - } elseif ($this->mode == 3) { + } elseif ($this->mpdf->restrictColorSpace == 3) { $c = $this->colorModeConverter->rgb2cmyk($c); } @@ -190,21 +189,21 @@ private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = []) */ private function restrictCmykaColorSpace($c, $color, &$PDFAXwarnings = []) { - if ($this->mpdf->PDFA && $this->mode != 3) { + if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) { if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { $PDFAXwarnings[] = "CMYK color with transparency specified '" . $color . "' (converted to RGB without transparency)"; } $c = $this->colorModeConverter->cmyk2rgb($c); $c = [3, $c[1], $c[2], $c[3]]; - } elseif ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) { + } elseif ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) { if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { $PDFAXwarnings[] = "CMYK color with transparency specified '" . $color . "' (converted to CMYK without transparency)"; } $c = $this->colorModeConverter->cmyk2rgb($c); $c = [3, $c[1], $c[2], $c[3]]; - } elseif ($this->mode == 1) { + } elseif ($this->mpdf->restrictColorSpace == 1) { $c = $this->colorModeConverter->cmyk2gray($c); - } elseif ($this->mode == 2) { + } elseif ($this->mpdf->restrictColorSpace == 2) { $c = $this->colorModeConverter->cmyk2rgb($c); } diff --git a/vendor/mpdf/mpdf/src/Config/ConfigVariables.php b/vendor/mpdf/mpdf/src/Config/ConfigVariables.php index 8eef5a80..fcbfc055 100644 --- a/vendor/mpdf/mpdf/src/Config/ConfigVariables.php +++ b/vendor/mpdf/mpdf/src/Config/ConfigVariables.php @@ -152,6 +152,7 @@ public function __construct() 'PDFA' => false, // Overrides warnings making changes when possible to force PDFA1-b compliance 'PDFAauto' => false, + 'PDFAversion' => '1-B', // Colour profile OutputIntent // sRGB_IEC61966-2-1 (=default if blank and PDFA), or other added .icc profile @@ -515,7 +516,7 @@ public function __construct() 'curlExecutionTimeout' => null, 'curlProxy' => null, 'curlProxyAuth' => null, - 'curlUserAgent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0.1', + 'curlUserAgent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0', 'exposeVersion' => true, ]; diff --git a/vendor/mpdf/mpdf/src/Config/FontVariables.php b/vendor/mpdf/mpdf/src/Config/FontVariables.php index 777ebf8a..a3694735 100644 --- a/vendor/mpdf/mpdf/src/Config/FontVariables.php +++ b/vendor/mpdf/mpdf/src/Config/FontVariables.php @@ -198,7 +198,7 @@ public function __construct() 'useOTL' => 0xFF, ], "eeyekunicode" => [/* Meetei Mayek */ - 'R' => "Eeyek.ttf", + 'R' => "Eeyek-Regular.ttf", ], "lannaalif" => [/* Tai Tham */ 'R' => "lannaalif-v1-03.ttf", diff --git a/vendor/mpdf/mpdf/src/Container/ContainerInterface.php b/vendor/mpdf/mpdf/src/Container/ContainerInterface.php new file mode 100644 index 00000000..e0fb0d5c --- /dev/null +++ b/vendor/mpdf/mpdf/src/Container/ContainerInterface.php @@ -0,0 +1,12 @@ +services = $services; + } + + public function get($id) + { + if (!$this->has($id)) { + throw new \Mpdf\Container\NotFoundException(sprintf('Unable to find service of key "%s"', $id)); + } + + return $this->services[$id]; + } + + public function has($id) + { + return isset($this->services[$id]); + } + + public function getServices() + { + return $this->services; + } + +} diff --git a/vendor/mpdf/mpdf/src/CssManager.php b/vendor/mpdf/mpdf/src/CssManager.php index 1ab5c14b..aeaf177a 100644 --- a/vendor/mpdf/mpdf/src/CssManager.php +++ b/vendor/mpdf/mpdf/src/CssManager.php @@ -5,6 +5,8 @@ use Mpdf\Color\ColorConverter; use Mpdf\Css\TextVars; use Mpdf\File\StreamWrapperChecker; +use Mpdf\Http\ClientInterface; +use Mpdf\PsrHttpMessageShim\Request; use Mpdf\Utils\Arrays; use Mpdf\Utils\UtfString; @@ -31,6 +33,11 @@ class CssManager */ private $colorConverter; + /** + * @var \Mpdf\AssetFetcher + */ + private $assetFetcher; + var $tablecascadeCSS; var $cascadeCSS; @@ -47,26 +54,21 @@ class CssManager var $cell_border_dominance_T; - /** - * @var \Mpdf\RemoteContentFetcher - */ - private $remoteContentFetcher; - - public function __construct(Mpdf $mpdf, Cache $cache, SizeConverter $sizeConverter, ColorConverter $colorConverter, RemoteContentFetcher $remoteContentFetcher) + public function __construct(Mpdf $mpdf, Cache $cache, SizeConverter $sizeConverter, ColorConverter $colorConverter, AssetFetcher $assetFetcher) { $this->mpdf = $mpdf; $this->cache = $cache; $this->sizeConverter = $sizeConverter; + $this->assetFetcher = $assetFetcher; $this->tablecascadeCSS = []; $this->CSS = []; $this->cascadeCSS = []; $this->tbCSSlvl = 0; $this->colorConverter = $colorConverter; - $this->remoteContentFetcher = $remoteContentFetcher; } - function ReadCSS($html) + public function ReadCSS($html) { preg_match_all('/]*media=["\']([^"\'>]*)["\'].*?<\/style>/is', $html, $m); $count_m = count($m[0]); @@ -156,7 +158,18 @@ function ReadCSS($html) $this->mpdf->GetFullPath($path); - $CSSextblock = $this->getFileContents($path); + // mPDF 5.7.3 + if (strpos($path, '//') === false) { + $path = preg_replace('/\.css\?.*$/', '.css', $path); + } + + $CSSextblock = $this->assetFetcher->fetchDataFromPath($path); + + if (!$CSSextblock) { + $path = $this->normalizePath($path); + $CSSextblock = $this->assetFetcher->fetchDataFromPath($path); + } + if ($CSSextblock) { // look for embedded @import stylesheets in other stylesheets // and fix url paths (including background-images) relative to stylesheet @@ -2279,25 +2292,8 @@ function _nthchild($f, $c) return $select; } - private function getFileContents($path) + private function normalizePath($path) { - // If local file try using local path (? quicker, but also allowed even if allow_url_fopen false) - $wrapperChecker = new StreamWrapperChecker($this->mpdf); - if ($wrapperChecker->hasBlacklistedStreamWrapper($path)) { - throw new \Mpdf\MpdfException('File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); - } - - // mPDF 5.7.3 - if (strpos($path, '//') === false) { - $path = preg_replace('/\.css\?.*$/', '.css', $path); - } - - $contents = @file_get_contents($path); - - if ($contents) { - return $contents; - } - if ($this->mpdf->basepathIsLocal) { $tr = parse_url($path); @@ -2309,26 +2305,17 @@ private function getFileContents($path) // WriteHTML parses all paths to full URLs; may be local file name // DOCUMENT_ROOT is not returned on IIS if (!empty($tr['scheme']) && $tr['host'] && !empty($_SERVER['DOCUMENT_ROOT'])) { - $localpath = $_SERVER['DOCUMENT_ROOT'] . $tr['path']; - } elseif ($docroot) { - $localpath = $docroot . $tr['path']; - } else { - $localpath = $path; + return $_SERVER['DOCUMENT_ROOT'] . $tr['path']; } - $contents = @file_get_contents($localpath); - - } else { // if not use full URL - - try { - $contents = $this->remoteContentFetcher->getFileContentsByCurl($path); - } catch (\Mpdf\MpdfException $e) { - // Ignore error + if ($docroot) { + return $docroot . $tr['path']; } + return $path; } - return $contents; + return $path; } } diff --git a/vendor/mpdf/mpdf/src/Exception/AssetFetchingException.php b/vendor/mpdf/mpdf/src/Exception/AssetFetchingException.php new file mode 100644 index 00000000..46fd0251 --- /dev/null +++ b/vendor/mpdf/mpdf/src/Exception/AssetFetchingException.php @@ -0,0 +1,8 @@ +mpdf->SetTColor($objattr['color']); - } else { - $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); - } + $this->mpdf->SetTColor(isset($objattr['color']) ? $objattr['color'] : $this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); $fieldalign = $rtlalign; @@ -218,8 +214,11 @@ function print_ob_text($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $js[] = ['K', $objattr['onKeystroke']]; } + if (!empty($objattr['use_auto_fontsize']) && $objattr['use_auto_fontsize'] === true) { + $this->mpdf->FontSizePt = 0.0; + } + $this->SetFormText($w, $h, $objattr['fieldname'], $val, $val, $objattr['title'], $flags, $fieldalign, false, (isset($objattr['maxlength']) ? $objattr['maxlength'] : false), $js, (isset($objattr['background-col']) ? $objattr['background-col'] : false), (isset($objattr['border-col']) ? $objattr['border-col'] : false)); - $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } else { @@ -227,6 +226,7 @@ function print_ob_text($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $h -= $this->form_element_spacing['input']['outer']['v'] * 2 / $k; $this->mpdf->x += $this->form_element_spacing['input']['outer']['h'] / $k; $this->mpdf->y += $this->form_element_spacing['input']['outer']['v'] / $k; + // Chop texto to max length $w-inner-padding while ($this->mpdf->GetStringWidth($texto) > $w - ($this->form_element_spacing['input']['inner']['h'] * 2)) { $texto = mb_substr($texto, 0, mb_strlen($texto, $this->mpdf->mb_enc) - 1, $this->mpdf->mb_enc); @@ -235,7 +235,8 @@ function print_ob_text($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) // DIRECTIONALITY if (preg_match('/([' . $this->mpdf->pregRTLchars . '])/u', $texto)) { $this->mpdf->biDirectional = true; - } // *RTL* + } + // Use OTL OpenType Table Layout - GSUB & GPOS if (!empty($this->mpdf->CurrentFont['useOTL'])) { $texto = $this->otl->applyOTL($texto, $this->mpdf->CurrentFont['useOTL']); @@ -245,6 +246,7 @@ function print_ob_text($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $this->mpdf->magic_reverse_dir($texto, $this->mpdf->directionality, $OTLdata); $this->mpdf->SetLineWidth(0.2 / $k); + if (!empty($objattr['disabled'])) { $this->mpdf->SetFColor($this->colorConverter->convert(225, $this->mpdf->PDFAXwarnings)); $this->mpdf->SetTColor($this->colorConverter->convert(127, $this->mpdf->PDFAXwarnings)); @@ -255,6 +257,7 @@ function print_ob_text($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $this->mpdf->SetFColor($this->colorConverter->convert(250, $this->mpdf->PDFAXwarnings)); $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } + $this->mpdf->Cell($w, $h, $texto, 1, 0, $rtlalign, 1, '', 0, $this->form_element_spacing['input']['inner']['h'] / $k, $this->form_element_spacing['input']['inner']['h'] / $k, 'M', 0, false, $OTLdata); $this->mpdf->SetFColor($this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings)); $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); @@ -267,34 +270,45 @@ function print_ob_textarea($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) if ($this->mpdf->useActiveForms) { $flags = [self::FLAG_TEXTAREA]; + if (!empty($objattr['disabled']) || !empty($objattr['readonly'])) { $flags[] = self::FLAG_READONLY; } + if (!empty($objattr['disabled'])) { $flags[] = self::FLAG_NO_EXPORT; $objattr['color'] = [3, 128, 128, 128]; // gray out disabled } + if (!empty($objattr['required'])) { $flags[] = self::FLAG_REQUIRED; } + if (!isset($objattr['spellcheck']) || !$objattr['spellcheck']) { $flags[] = self::FLAG_NO_SPELLCHECK; } + if (!empty($objattr['donotscroll'])) { $flags[] = self::FLAG_NO_SCROLL; } + if (isset($objattr['color'])) { $this->mpdf->SetTColor($objattr['color']); } else { $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } + $fieldalign = $rtlalign; + if ($texto === ' ') { $texto = ''; - } // mPDF 5.3.24 + } + + // mPDF 5.3.24 if (!empty($objattr['text_align'])) { $fieldalign = $objattr['text_align']; } + // mPDF 5.3.25 $js = []; if (!empty($objattr['onCalculate'])) { @@ -309,14 +323,24 @@ function print_ob_textarea($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) if (!empty($objattr['onKeystroke'])) { $js[] = ['K', $objattr['onKeystroke']]; } + + if (!empty($objattr['use_auto_fontsize']) && $objattr['use_auto_fontsize'] === true) { + $this->mpdf->FontSizePt = 0.0; + } + $this->SetFormText($w, $h, $objattr['fieldname'], $texto, $texto, (isset($objattr['title']) ? $objattr['title'] : ''), $flags, $fieldalign, false, -1, $js, (isset($objattr['background-col']) ? $objattr['background-col'] : false), (isset($objattr['border-col']) ? $objattr['border-col'] : false)); $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); + } else { + $w -= $this->form_element_spacing['textarea']['outer']['h'] * 2 / $k; $h -= $this->form_element_spacing['textarea']['outer']['v'] * 2 / $k; + $this->mpdf->x += $this->form_element_spacing['textarea']['outer']['h'] / $k; $this->mpdf->y += $this->form_element_spacing['textarea']['outer']['v'] / $k; + $this->mpdf->SetLineWidth(0.2 / $k); + if (!empty($objattr['disabled'])) { $this->mpdf->SetFColor($this->colorConverter->convert(225, $this->mpdf->PDFAXwarnings)); $this->mpdf->SetTColor($this->colorConverter->convert(127, $this->mpdf->PDFAXwarnings)); @@ -325,8 +349,9 @@ function print_ob_textarea($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } else { $this->mpdf->SetFColor($this->colorConverter->convert(250, $this->mpdf->PDFAXwarnings)); - $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); + $this->mpdf->SetTColor(isset($objattr['color']) ? $objattr['color'] : $this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } + $this->mpdf->Rect($this->mpdf->x, $this->mpdf->y, $w, $h, 'DF'); $ClipPath = sprintf('q %.3F %.3F %.3F %.3F re W n ', $this->mpdf->x * Mpdf::SCALE, ($this->mpdf->h - $this->mpdf->y) * Mpdf::SCALE, $w * Mpdf::SCALE, -$h * Mpdf::SCALE); $this->writer->write($ClipPath); @@ -338,6 +363,7 @@ function print_ob_textarea($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) if ($texto != '') { $this->mpdf->MultiCell($w, $this->mpdf->FontSize * $this->textarea_lineheight, $texto, 0, '', 0, '', $blockdir, true, $objattr['OTLdata'], $objattr['rows']); } + $this->writer->write('Q'); $this->mpdf->SetFColor($this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings)); $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); @@ -366,18 +392,22 @@ function print_ob_select($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $flags[] = self::FLAG_EDITABLE; } } + // only allow spellcheck if combo and editable if ((!isset($objattr['spellcheck']) || !$objattr['spellcheck']) || (isset($objattr['size']) && $objattr['size'] > 1) || (!isset($objattr['editable']) || !$objattr['editable'])) { $flags[] = self::FLAG_NO_SPELLCHECK; } + if (isset($objattr['subtype']) && $objattr['subtype'] === 'PASSWORD') { $flags[] = self::FLAG_PASSWORD; } + if (!empty($objattr['onChange'])) { $js = $objattr['onChange']; } else { $js = ''; } // mPDF 5.3.37 + $data = ['VAL' => [], 'OPT' => [], 'SEL' => [],]; if (isset($objattr['items'])) { for ($i = 0; $i < count($objattr['items']); $i++) { @@ -389,16 +419,20 @@ function print_ob_select($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) } } } + if (count($data['SEL']) === 0 && $this->formSelectDefaultOption) { $data['SEL'][] = 0; } + if (isset($objattr['color'])) { $this->mpdf->SetTColor($objattr['color']); } else { $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); } + $this->SetFormChoice($w, $h, $objattr['fieldname'], $flags, $data, $rtlalign, $js); $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); + } else { $this->mpdf->SetLineWidth(0.2 / $k); if (!empty($objattr['disabled'])) { @@ -480,12 +514,11 @@ function print_ob_button($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $flags[] = self::FLAG_NO_EXPORT; $objattr['color'] = [3, 128, 128, 128]; } - if (isset($objattr['color'])) { - $this->mpdf->SetTColor($objattr['color']); - } else { - $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); - } + + $this->mpdf->SetTColor(isset($objattr['color']) ? $objattr['color'] : $this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); + if (isset($objattr['subtype'])) { + if ($objattr['subtype'] === 'RESET') { $this->SetFormButtonText($objattr['value']); $this->SetFormReset($w, $h, $objattr['fieldname'], $objattr['value'], $objattr['title'], $flags, (isset($objattr['background-col']) ? $objattr['background-col'] : false), (isset($objattr['border-col']) ? $objattr['border-col'] : false), (isset($objattr['noprint']) ? $objattr['noprint'] : false)); @@ -505,24 +538,32 @@ function print_ob_button($objattr, $w, $h, $texto, $rtlalign, $k, $blockdir) $this->SetJSButton($w, $h, $objattr['fieldname'], $objattr['value'], $js, 0, $objattr['title'], $flags, false, (isset($objattr['background-col']) ? $objattr['background-col'] : false), (isset($objattr['border-col']) ? $objattr['border-col'] : false), (isset($objattr['noprint']) ? $objattr['noprint'] : false)); } } + $this->mpdf->SetTColor($this->colorConverter->convert(0, $this->mpdf->PDFAXwarnings)); + } else { + $this->mpdf->SetLineWidth(0.2 / $k); $this->mpdf->SetFColor($this->colorConverter->convert(190, $this->mpdf->PDFAXwarnings)); + $w -= $this->form_element_spacing['button']['outer']['h'] * 2 / $k; $h -= $this->form_element_spacing['button']['outer']['v'] * 2 / $k; + $this->mpdf->x += $this->form_element_spacing['button']['outer']['h'] / $k; $this->mpdf->y += $this->form_element_spacing['button']['outer']['v'] / $k; $this->mpdf->RoundedRect($this->mpdf->x, $this->mpdf->y, $w, $h, 0.5 / $k, 'DF'); + $w -= $this->form_element_spacing['button']['inner']['h'] * 2 / $k; $h -= $this->form_element_spacing['button']['inner']['v'] * 2 / $k; + $this->mpdf->x += $this->form_element_spacing['button']['inner']['h'] / $k; $this->mpdf->y += $this->form_element_spacing['button']['inner']['v'] / $k; // DIRECTIONALITY if (preg_match('/([' . $this->mpdf->pregRTLchars . '])/u', $texto)) { $this->mpdf->biDirectional = true; - } // *RTL* + } + // Use OTL OpenType Table Layout - GSUB & GPOS if (!empty($this->mpdf->CurrentFont['useOTL'])) { $texto = $this->otl->applyOTL($texto, $this->mpdf->CurrentFont['useOTL']); diff --git a/vendor/mpdf/mpdf/src/Gradient.php b/vendor/mpdf/mpdf/src/Gradient.php index 95062e3c..e886f419 100644 --- a/vendor/mpdf/mpdf/src/Gradient.php +++ b/vendor/mpdf/mpdf/src/Gradient.php @@ -53,7 +53,7 @@ public function CoonsPatchMesh($x, $y, $w, $h, $patch_array = [], $x_min = 0, $x $this->mpdf->gradients[$n]['stream'] = ''; for ($i = 0; $i < count($patch_array); $i++) { - $this->mpdf->gradients[$n]['stream'].=chr($patch_array[$i]['f']); //start with the edge flag as 8 bit + $this->mpdf->gradients[$n]['stream'] .= chr($patch_array[$i]['f']); //start with the edge flag as 8 bit for ($j = 0; $j < count($patch_array[$i]['points']); $j++) { @@ -93,7 +93,7 @@ public function CoonsPatchMesh($x, $y, $w, $h, $patch_array = [], $x_min = 0, $x $trans = true; } } elseif ($colspace === 'Gray') { - $this->mpdf->gradients[$n]['stream'].= $patch_array[$i]['colors'][$j][1]; + $this->mpdf->gradients[$n]['stream'] .= $patch_array[$i]['colors'][$j][1]; if ($patch_array[$i]['colors'][$j][2] == 1) { $trans = true; } // transparency converted from rgba or cmyka() @@ -620,15 +620,19 @@ private function parseMozLinearGradient($m, $repeat) $g['colorspace'] = 'RGB'; $g['extend'] = ['true', 'true']; $v = trim($m[1]); + // Change commas inside e.g. rgb(x,x,x) while (preg_match('/(\([^\)]*?),/', $v)) { $v = preg_replace('/(\([^\)]*?),/', '\\1@', $v); } + // Remove spaces inside e.g. rgb(x, x, x) while (preg_match('/(\([^\)]*?)[ ]/', $v)) { $v = preg_replace('/(\([^\)]*?)[ ]/', '\\1', $v); } + $bgr = preg_split('/\s*,\s*/', $v); + for ($i = 0; $i < count($bgr); $i++) { $bgr[$i] = preg_replace('/@/', ',', $bgr[$i]); } @@ -645,8 +649,10 @@ private function parseMozLinearGradient($m, $repeat) $startStops = 0; } } + // first part a valid point/angle? if ($startStops === 1) { // default values + // [ || ,] = [<% em px left center right bottom top> || ,] if (preg_match('/([\-]*[0-9\.]+)(deg|grad|rad)/i', $bgr[0], $m)) { $angle = $m[1] + 0; @@ -662,16 +668,19 @@ private function parseMozLinearGradient($m, $repeat) } elseif (trim($first[count($first) - 1]) === '0') { $angle = 0; } + if (stripos($bgr[0], 'left') !== false) { - $startx = 0; - } elseif (stripos($bgr[0], 'right') !== false) { $startx = 1; + } elseif (stripos($bgr[0], 'right') !== false) { + $startx = 0; } + if (stripos($bgr[0], 'top') !== false) { $starty = 1; } elseif (stripos($bgr[0], 'bottom') !== false) { $starty = 0; } + // Check for %? ?% or %% if (preg_match('/(\d+)[%]/i', $first[0], $m)) { $startx = $m[1] / 100; @@ -681,6 +690,7 @@ private function parseMozLinearGradient($m, $repeat) $startx = $m[1]; } } + if (isset($first[1]) && preg_match('/(\d+)[%]/i', $first[1], $m)) { $starty = 1 - ($m[1] / 100); } elseif (!isset($starty) && isset($first[1]) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $first[1], $m)) { @@ -689,12 +699,15 @@ private function parseMozLinearGradient($m, $repeat) $starty = $m[1]; } } + if (isset($startx) && !isset($starty)) { $starty = 0.5; } + if (!isset($startx) && isset($starty)) { $startx = 0.5; } + } else { // If neither a or is specified, i.e. the entire function consists of only values, // the gradient axis starts from the top of the box and runs vertically downwards, ending at the bottom of @@ -704,31 +717,41 @@ private function parseMozLinearGradient($m, $repeat) $endy = 0; $endx = 0.5; } + if (!isset($startx)) { $startx = false; } + if (!isset($starty)) { $starty = false; } + if (!isset($endx)) { $endx = false; } + if (!isset($endy)) { $endy = false; } + if (!isset($angle)) { $angle = false; } + $g['coords'] = [$startx, $starty, $endx, $endy, $angle, $repeat]; $g['stops'] = []; + for ($i = $startStops; $i < count($bgr); $i++) { + // parse stops $el = preg_split('/\s+/', trim($bgr[$i])); // mPDF 5.3.74 $col = $this->colorConverter->convert($el[0], $this->mpdf->PDFAXwarnings); + if (!$col) { $col = $this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings); } + if ($col[0] == 1) { $g['colorspace'] = 'Gray'; } elseif ($col[0] == 4 || $col[0] == 6) { @@ -737,6 +760,7 @@ private function parseMozLinearGradient($m, $repeat) $g['stops'][] = $this->getStop($col, $el, true); } + return $g; } diff --git a/vendor/mpdf/mpdf/src/Http/ClientInterface.php b/vendor/mpdf/mpdf/src/Http/ClientInterface.php new file mode 100644 index 00000000..a5499eea --- /dev/null +++ b/vendor/mpdf/mpdf/src/Http/ClientInterface.php @@ -0,0 +1,12 @@ +mpdf = $mpdf; $this->logger = $logger; } - public function getFileContentsByCurl($url) + public function sendRequest(RequestInterface $request) { + if (null === $request->getUri()) { + return (new Response()); + } + + $url = $request->getUri(); + $this->logger->debug(sprintf('Fetching (cURL) content of remote URL "%s"', $url), ['context' => LogContext::REMOTE_CONTENT]); + $response = new Response(); + $ch = curl_init($url); curl_setopt($ch, CURLOPT_USERAGENT, $this->mpdf->curlUserAgent); @@ -61,6 +66,22 @@ public function getFileContentsByCurl($url) } } + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + static function ($curl, $header) use (&$response) { + $len = strlen($header); + $header = explode(':', $header, 2); + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $response = $response->withHeader(trim($header[0]), trim($header[1])); + + return $len; + } + ); + $data = curl_exec($ch); if (curl_error($ch)) { @@ -70,83 +91,37 @@ public function getFileContentsByCurl($url) if ($this->mpdf->debug) { throw new \Mpdf\MpdfException($message); } + + $this->closeCurl($ch); + + return $response; } $info = curl_getinfo($ch); - if (isset($info['http_code']) && $info['http_code'] !== 200) { + if (isset($info['http_code']) && !str_starts_with((string) $info['http_code'], '2')) { $message = sprintf('HTTP error: %d', $info['http_code']); $this->logger->error($message, ['context' => LogContext::REMOTE_CONTENT]); if ($this->mpdf->debug) { throw new \Mpdf\MpdfException($message); } - } - - curl_close($ch); - - return $data; - } - - public function getFileContentsBySocket($url) - { - $this->logger->debug(sprintf('Fetching (socket) content of remote URL "%s"', $url), ['context' => LogContext::REMOTE_CONTENT]); - // mPDF 5.7.3 - - $timeout = 1; - $p = parse_url($url); - - $file = Arrays::get($p, 'path', ''); - $scheme = Arrays::get($p, 'scheme', ''); - $port = Arrays::get($p, 'port', 80); - $prefix = ''; - - if ($scheme === 'https') { - $prefix = 'ssl://'; - $port = Arrays::get($p, 'port', 443); - } - - $query = Arrays::get($p, 'query', null); - if ($query) { - $file .= '?' . $query; - } - - if (!($fh = @fsockopen($prefix . $p['host'], $port, $errno, $errstr, $timeout))) { - $this->logger->error(sprintf('Socket error "%s": "%s"', $errno, $errstr), ['context' => LogContext::REMOTE_CONTENT]); - return false; - } - - $getstring = 'GET ' . $file . " HTTP/1.0 \r\n" . - 'Host: ' . $p['host'] . " \r\n" . - "Connection: close\r\n\r\n"; - - fwrite($fh, $getstring); - - // Get rid of HTTP header - $s = fgets($fh, 1024); - if (!$s) { - return false; - } - - while (!feof($fh)) { - $s = fgets($fh, 1024); - if ($s === "\r\n") { - break; - } - } - $data = ''; + $this->closeCurl($ch); - while (!feof($fh)) { - $data .= fgets($fh, 1024); + return $response->withStatus($info['http_code']); } - fclose($fh); + $this->closeCurl($ch); - return $data; + return $response + ->withStatus($info['http_code']) + ->withBody(Stream::create($data)); } - public function setLogger(LoggerInterface $logger) + private function closeCurl($ch) { - $this->logger = $logger; + if (PHP_VERSION_ID < 80000) { + curl_close($ch); + } } } diff --git a/vendor/mpdf/mpdf/src/Http/Exception/ClientException.php b/vendor/mpdf/mpdf/src/Http/Exception/ClientException.php new file mode 100644 index 00000000..c01c41b4 --- /dev/null +++ b/vendor/mpdf/mpdf/src/Http/Exception/ClientException.php @@ -0,0 +1,8 @@ +logger = $logger; + } + + public function sendRequest(RequestInterface $request) + { + if (null === $request->getUri()) { + return (new Response()); // @todo throw exception + } + + $url = $request->getUri(); + + if (is_string($url)) { + $url = new Uri($url); + } + + $timeout = 1; + + $file = $url->getPath() ?: '/'; + $scheme = $url->getScheme(); + $port = $url->getPort() ?: 80; + $prefix = ''; + + if ($scheme === 'https') { + $prefix = 'ssl://'; + $port = $url->getPort() ?: 443; + } + + $query = $url->getQuery(); + if ($query) { + $file .= '?' . $query; + } + + $socketPath = $prefix . $url->getHost(); + + $this->logger->debug(sprintf('Opening socket on %s:%s of URL "%s"', $socketPath, $port, $request->getUri()), ['context' => LogContext::REMOTE_CONTENT]); + + $response = new Response(); + + if (!($fh = @fsockopen($socketPath, $port, $errno, $errstr, $timeout))) { + $this->logger->error(sprintf('Socket error "%s": "%s"', $errno, $errstr), ['context' => LogContext::REMOTE_CONTENT]); + + return $response; + } + + $getRequest = 'GET ' . $file . ' HTTP/1.1' . "\r\n" . + 'Host: ' . $url->getHost() . " \r\n" . + 'Connection: close' . "\r\n\r\n"; + + fwrite($fh, $getRequest); + + $httpHeader = fgets($fh, 1024); + if (!$httpHeader) { + return $response; // @todo throw exception + } + + preg_match('@HTTP/(?P[\d\.]+) (?P[\d]+) .*@', $httpHeader, $parsedHeader); + + if (!$parsedHeader) { + return $response; // @todo throw exception + } + + $response = $response->withStatus($parsedHeader['httpStatusCode']); + + while (!feof($fh)) { + $s = fgets($fh, 1024); + if ($s === "\r\n") { + break; + } + preg_match('/^(?P.*?): ?(?P.*)$/', $s, $parsedHeader); + if (!$parsedHeader) { + continue; + } + $response = $response->withHeader($parsedHeader['headerName'], trim($parsedHeader['headerValue'])); + } + + $body = ''; + + while (!feof($fh)) { + $line = fgets($fh, 1024); + $body .= $line; + } + + fclose($fh); + + $stream = Stream::create($body); + $stream->rewind(); + + return $response + ->withBody($stream); + } + +} diff --git a/vendor/mpdf/mpdf/src/Hyphenator.php b/vendor/mpdf/mpdf/src/Hyphenator.php index e1243abc..71cce937 100644 --- a/vendor/mpdf/mpdf/src/Hyphenator.php +++ b/vendor/mpdf/mpdf/src/Hyphenator.php @@ -44,7 +44,11 @@ public function hyphenateWord($word, $currptr) { // Do everything inside this function in utf-8 // Don't hyphenate web addresses - if (preg_match('/^(http:|www\.)/', $word)) { + if (preg_match('/^(http:|https:|www\.)/', $word)) { + return -1; + } + // Don't hyphenate email addresses + if (preg_match('/^[a-zA-Z0-9-_.+]+@[a-zA-Z0-9-_.]+/', $word)) { return -1; } diff --git a/vendor/mpdf/mpdf/src/Image/ImageProcessor.php b/vendor/mpdf/mpdf/src/Image/ImageProcessor.php index da587c25..98fd83f0 100644 --- a/vendor/mpdf/mpdf/src/Image/ImageProcessor.php +++ b/vendor/mpdf/mpdf/src/Image/ImageProcessor.php @@ -2,24 +2,26 @@ namespace Mpdf\Image; +use Mpdf\AssetFetcher; use Mpdf\Cache; use Mpdf\Color\ColorConverter; use Mpdf\Color\ColorModeConverter; use Mpdf\CssManager; -use Mpdf\File\StreamWrapperChecker; use Mpdf\Gif\Gif; use Mpdf\Language\LanguageToFontInterface; use Mpdf\Language\ScriptToLanguageInterface; use Mpdf\Log\Context as LogContext; use Mpdf\Mpdf; use Mpdf\Otl; -use Mpdf\RemoteContentFetcher; +use Mpdf\PsrLogAwareTrait\PsrLogAwareTrait; use Mpdf\SizeConverter; use Psr\Log\LoggerInterface; class ImageProcessor implements \Psr\Log\LoggerAwareInterface { + use PsrLogAwareTrait; + /** * @var \Mpdf\Mpdf */ @@ -86,14 +88,9 @@ class ImageProcessor implements \Psr\Log\LoggerAwareInterface public $scriptToLanguage; /** - * @var \Mpdf\RemoteContentFetcher - */ - private $remoteContentFetcher; - - /** - * @var \Psr\Log\LoggerInterface + * @var \Mpdf\AssetFetcher */ - public $logger; + private $assetFetcher; public function __construct( Mpdf $mpdf, @@ -105,7 +102,7 @@ public function __construct( Cache $cache, LanguageToFontInterface $languageToFont, ScriptToLanguageInterface $scriptToLanguage, - RemoteContentFetcher $remoteContentFetcher, + AssetFetcher $assetFetcher, LoggerInterface $logger ) { @@ -118,7 +115,7 @@ public function __construct( $this->cache = $cache; $this->languageToFont = $languageToFont; $this->scriptToLanguage = $scriptToLanguage; - $this->remoteContentFetcher = $remoteContentFetcher; + $this->assetFetcher = $assetFetcher; $this->logger = $logger; @@ -127,33 +124,8 @@ public function __construct( $this->failedImages = []; } - /** - * @param \Psr\Log\LoggerInterface - * - * @return self - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - - return $this; - } - - public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_srcpath = false, $interpolation = false) + public function getImage(&$file, $firstTime = true, $allowvector = true, $orig_srcpath = false, $interpolation = false) { - /** - * Prevents insecure PHP object injection through phar:// wrapper - * @see https://github.com/mpdf/mpdf/issues/949 - * @see https://github.com/mpdf/mpdf/issues/1381 - */ - $wrapperChecker = new StreamWrapperChecker($this->mpdf); - if ($wrapperChecker->hasBlacklistedStreamWrapper($file)) { - return $this->imageError($file, $firsttime, 'File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); - } - if ($wrapperChecker->hasBlacklistedStreamWrapper($orig_srcpath)) { - return $this->imageError($orig_srcpath, $firsttime, 'File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); - } - // mPDF 6 // firsttime i.e. whether to add to this->images - use false when calling iteratively // Image Data passed directly as var:varname @@ -163,23 +135,25 @@ public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_s if (preg_match('/var:\s*(.*)/', $file, $v)) { if (!isset($this->mpdf->imageVars[$v[1]])) { - return $this->imageError($file, $firsttime, 'Unknown image variable'); + return $this->imageError($file, $firstTime, 'Unknown image variable'); } + $data = $this->mpdf->imageVars[$v[1]]; $file = md5($data); } - if (preg_match('/data:image\/(gif|jpe?g|png|webp);base64,(.*)/', $file, $v)) { + if (preg_match('/data:image\/(gif|jpe?g|png|webp|svg\+xml);base64,(.*)/', $file, $v)) { $type = $v[1]; $data = base64_decode($v[2]); $file = md5($data); } // mPDF 5.7.4 URLs - if ($firsttime && $file && strpos($file, 'data:') !== 0) { + if ($firstTime && $file && strpos($file, 'data:') !== 0) { $file = str_replace(' ', '%20', $file); } - if ($firsttime && $orig_srcpath) { + + if ($firstTime && $orig_srcpath) { // If orig_srcpath is a relative file path (and not a URL), then it needs to be URL decoded if (strpos($orig_srcpath, 'data:') !== 0) { $orig_srcpath = str_replace(' ', '%20', $orig_srcpath); @@ -189,7 +163,6 @@ public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_s } } - $ppUx = 0; if ($orig_srcpath && isset($this->mpdf->images[$orig_srcpath])) { $file = $orig_srcpath; return $this->mpdf->images[$orig_srcpath]; @@ -208,995 +181,166 @@ public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_s return $this->mpdf->formobjects[$file]; } - if ($firsttime && isset($this->failedImages[$file])) { // Save re-trying image URL's which have already failed - return $this->imageError($file, $firsttime, ''); + if ($firstTime && isset($this->failedImages[$file])) { // Save re-trying image URL's which have already failed + return $this->imageError($file, $firstTime, ''); } - if (empty($data)) { - - $data = ''; - - if ($orig_srcpath && $this->mpdf->basepathIsLocal && $check = @fopen($orig_srcpath, 'rb')) { - fclose($check); - $file = $orig_srcpath; - $this->logger->debug(sprintf('Fetching (file_get_contents) content of file "%s" with local basepath', $file), ['context' => LogContext::REMOTE_CONTENT]); - $data = file_get_contents($file); - $type = $this->guesser->guess($data); - } - - if ($file && !$data && $check = @fopen($file, 'rb')) { - fclose($check); - $this->logger->debug(sprintf('Fetching (file_get_contents) content of file "%s" with non-local basepath', $file), ['context' => LogContext::REMOTE_CONTENT]); - $data = file_get_contents($file); - $type = $this->guesser->guess($data); - } - - if ((!$data || !$type) && function_exists('curl_init')) { // mPDF 5.7.4 - $data = $this->remoteContentFetcher->getFileContentsByCurl($file); // needs full url?? even on local (never needed for local) - if ($data) { - $type = $this->guesser->guess($data); - } - } - - if ((!$data || !$type) && !ini_get('allow_url_fopen')) { // only worth trying if remote file and !ini_get('allow_url_fopen') - $data = $this->remoteContentFetcher->getFileContentsBySocket($file); // needs full url?? even on local (never needed for local) - if ($data) { - $type = $this->guesser->guess($data); - } + if (!$data) { + try { + $data = $this->assetFetcher->fetchDataFromPath($file, $orig_srcpath); + } catch (\Mpdf\Exception\AssetFetchingException $e) { + return $this->imageError($orig_srcpath, $firstTime, $e->getMessage()); } } if (!$data) { - return $this->imageError($file, $firsttime, 'Could not find image file'); + return $this->imageError($file, $firstTime, 'Could not find image file'); } if ($type === null) { $type = $this->guesser->guess($data); } - if (($type === 'wmf' || $type === 'svg') && !$allowvector) { - return $this->imageError($file, $firsttime, 'WMF or SVG image file not supported in this context'); - } - - // SVG - if ($type === 'svg') { - $svg = new Svg($this->mpdf, $this->otl, $this->cssManager, $this, $this->sizeConverter, $this->colorConverter, $this->languageToFont, $this->scriptToLanguage); - $family = $this->mpdf->FontFamily; - $style = $this->mpdf->FontStyle; - $size = $this->mpdf->FontSizePt; - $info = $svg->ImageSVG($data); - // Restore font - if ($family) { - $this->mpdf->SetFont($family, $style, $size, false); - } - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing SVG file'); + if ($type === 'svg' || $type === 'svg+xml') { + if (!$allowvector) { + return $this->imageError($file, $firstTime, 'SVG image file not supported in this context'); } - $info['type'] = 'svg'; - $info['i'] = count($this->mpdf->formobjects) + 1; - $this->mpdf->formobjects[$file] = $info; - - return $info; + return $this->processSvg($data, $file, $firstTime); } - if ($type === 'webp') { // Convert webp images to JPG and treat them as such - - $im = @imagecreatefromstring($data); - - if (!function_exists('imagewebp') || false === $im) { - return $this->imageError($file, $firsttime, 'Missing GD support for WEBP images.'); - } - - $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); - $checkfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); - $check = @imagewebp($im, $checkfile); - - if (!$check) { - return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse WEBP image'); + if ($type === 'wmf') { + if (!$allowvector) { + return $this->imageError($file, $firstTime, 'WMF image file not supported in this context'); } + return $this->processWmf($data, $file, $firstTime); + } - @imagejpeg($im, $tempfile); - $data = file_get_contents($tempfile); - imagedestroy($im); - unlink($tempfile); - unlink($checkfile); + if ($type === 'webp') { + // Convert webp images to JPG and treat them as such + $data = $this->processWebp($data, $file, $firstTime); $type = 'jpeg'; - } // JPEG if ($type === 'jpeg' || $type === 'jpg') { - - $hdr = $this->jpgHeaderFromString($data); - if (!$hdr) { - return $this->imageError($file, $firsttime, 'Error parsing JPG header'); - } - - $a = $this->jpgDataFromHeader($hdr); - $channels = (int) $a[4]; - $j = strpos($data, 'JFIF'); - - if ($j) { - // Read resolution - $unitSp = ord(substr($data, $j + 7, 1)); - if ($unitSp > 0) { - $ppUx = $this->twoBytesToInt(substr($data, $j + 8, 2)); // horizontal pixels per meter, usually set to zero - if ($unitSp === 2) { // = dots per cm (if == 1 set as dpi) - $ppUx = round($ppUx / 10 * 25.4); - } - } - } - - if ($a[2] === 'DeviceCMYK' && ($this->mpdf->restrictColorSpace === 2 || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace !== 3))) { - - // convert to RGB image - if (!function_exists('gd_info')) { - throw new \Mpdf\MpdfException(sprintf('JPG image may not use CMYK color space (%s).', $file)); - } - - if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) { - $this->mpdf->PDFAXwarnings[] = sprintf('JPG image may not use CMYK color space - %s - (Image converted to RGB. NB This will alter the colour profile of the image.)', $file); - } - - $im = @imagecreatefromstring($data); - - if ($im) { - $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); - imageinterlace($im, false); - $check = @imagepng($im, $tempfile); - if (!$check) { - return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse JPG(CMYK) image'); - } - $info = $this->getImage($tempfile, false); - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse JPG(CMYK) image'); - } - imagedestroy($im); - unlink($tempfile); - $info['type'] = 'jpg'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - return $info; - } - - return $this->imageError($file, $firsttime, 'Error creating GD image file from JPG(CMYK) image'); - - } - - if ($a[2] === 'DeviceRGB' && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { - // Convert to CMYK image stream - nominally returned as type='png' - $info = $this->convertImage($data, $a[2], 'DeviceCMYK', $a[0], $a[1], $ppUx, false); - if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { - $this->mpdf->PDFAXwarnings[] = sprintf('JPG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); - } - - } elseif (($a[2] === 'DeviceRGB' || $a[2] === 'DeviceCMYK') && $this->mpdf->restrictColorSpace === 1) { - // Convert to Grayscale image stream - nominally returned as type='png' - $info = $this->convertImage($data, $a[2], 'DeviceGray', $a[0], $a[1], $ppUx, false); - - } else { - // mPDF 6 Detect Adobe APP14 Tag - //$pos = strpos($data, "\xFF\xEE\x00\x0EAdobe\0"); - //if ($pos !== false) { - //} - // mPDF 6 ICC profile - $offset = 0; - $icc = []; - while (($pos = strpos($data, "ICC_PROFILE\0", $offset)) !== false) { - // get ICC sequence length - $length = $this->twoBytesToInt(substr($data, $pos - 2, 2)) - 16; - $sn = max(1, ord($data[$pos + 12])); - $nom = max(1, ord($data[$pos + 13])); - $icc[$sn - 1] = substr($data, $pos + 14, $length); - $offset = ($pos + 14 + $length); - } - // order and compact ICC segments - if (count($icc) > 0) { - ksort($icc); - $icc = implode('', $icc); - if (substr($icc, 36, 4) !== 'acsp') { - // invalid ICC profile - $icc = false; - } - $input = substr($icc, 16, 4); - $output = substr($icc, 20, 4); - // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab - if ($input !== 'RGB ' || $output !== 'XYZ ') { - $icc = false; - } - } else { - $icc = false; - } - - $info = ['w' => $a[0], 'h' => $a[1], 'cs' => $a[2], 'bpc' => $a[3], 'f' => 'DCTDecode', 'data' => $data, 'type' => 'jpg', 'ch' => $channels, 'icc' => $icc]; - if ($ppUx) { - $info['set-dpi'] = $ppUx; - } - } - - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing or converting JPG image'); - } - - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - + return $this->processJpg($data, $file, $firstTime, $interpolation); } if ($type === 'png') { + return $this->processPng($data, $file, $firstTime, $interpolation); + } - // Check signature - if (strpos($data, chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) !== 0) { - return $this->imageError($file, $firsttime, 'Error parsing PNG identifier'); - } - - // Read header chunk - if (substr($data, 12, 4) !== 'IHDR') { - return $this->imageError($file, $firsttime, 'Incorrect PNG file (no IHDR block found)'); - } - - $w = $this->fourBytesToInt(substr($data, 16, 4)); - $h = $this->fourBytesToInt(substr($data, 20, 4)); - $bpc = ord(substr($data, 24, 1)); - $errpng = false; - $pngalpha = false; - $channels = 0; - - // if($bpc>8) { $errpng = 'not 8-bit depth'; } // mPDF 6 Allow through to be handled as native PNG - $ct = ord(substr($data, 25, 1)); - - if ($ct === 0) { - $colspace = 'DeviceGray'; - $channels = 1; - } elseif ($ct === 2) { - $colspace = 'DeviceRGB'; - $channels = 3; - } elseif ($ct === 3) { - $colspace = 'Indexed'; - $channels = 1; - } elseif ($ct === 4) { - $colspace = 'DeviceGray'; - $channels = 1; - $errpng = 'alpha channel'; - $pngalpha = true; - } else { - $colspace = 'DeviceRGB'; - $channels = 3; - $errpng = 'alpha channel'; - $pngalpha = true; - } + if ($type === 'gif') { // GIF + return $this->processGif($data, $file, $firstTime, $interpolation); + } - if ($ct < 4 && strpos($data, 'tRNS') !== false) { - $errpng = 'transparency'; - $pngalpha = true; - } // mPDF 6 + if ($type === 'bmp') { + return $this->processBmp($data, $file, $firstTime, $interpolation); + } - if ($ct === 3 && strpos($data, 'iCCP') !== false) { - $errpng = 'indexed plus ICC'; - } // mPDF 6 + return $this->processUnknownType($data, $file, $firstTime, $interpolation); + } - // $pngalpha is used as a FLAG of any kind of transparency which COULD be tranferred to an alpha channel - // incl. single-color tarnsparency, depending which type of handling occurs later - if (ord(substr($data, 26, 1)) !== 0) { - $errpng = 'compression method'; - } // only 0 should be specified + private function convertImage(&$data, $colspace, $targetcs, $w, $h, $dpi, $mask, $gamma_correction = false, $pngcolortype = false) + { + if (!function_exists('gd_info')) { + return $this->imageError('', false, 'GD library needed to parse image files'); + } - if (ord(substr($data, 27, 1)) !== 0) { - $errpng = 'filter method'; - } // only 0 should be specified + if ($this->mpdf->PDFA || $this->mpdf->PDFX) { + $mask = false; + } - if (ord(substr($data, 28, 1)) !== 0) { - $errpng = 'interlaced file'; - } + $im = @imagecreatefromstring($data); + $info = []; + $bpc = ord(substr($data, 24, 1)); - $j = strpos($data, 'pHYs'); - if ($j) { - //Read resolution - $unitSp = ord(substr($data, $j + 12, 1)); - if ($unitSp === 1) { - $ppUx = $this->fourBytesToInt(substr($data, $j + 4, 4)); // horizontal pixels per meter, usually set to zero - $ppUx = round($ppUx / 1000 * 25.4); - } - } + if ($im) { + $imgdata = ''; + $mimgdata = ''; + $minfo = []; // mPDF 6 Gamma correction - $gamma = 0; - $gAMA = 0; - $j = strpos($data, 'gAMA'); - if ($j && strpos($data, 'sRGB') === false) { // sRGB colorspace - overrides gAMA - $gAMA = $this->fourBytesToInt(substr($data, $j + 4, 4)); // Gamma value times 100000 - $gAMA /= 100000; - - // http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html - // "If the source file's gamma value is greater than 1.0, it is probably a display system exponent,..." - // ("..and you should use its reciprocal for the PNG gamma.") - //if ($gAMA > 1) { $gAMA = 1/$gAMA; } - // (Some) Applications seem to ignore it... appearing how it was probably intended - // Test Case - image(s) on http://www.w3.org/TR/CSS21/intro.html - PNG has gAMA set as 1.45454 - // Probably unintentional as mentioned above and should be 0.45454 which is 1 / 2.2 - // Tested on Windows PC - // Firefox and Opera display gray as 234 (correct, but looks wrong) - // IE9 and Safari display gray as 193 (incorrect but looks right) - // See test different gamma chunks at http://www.libpng.org/pub/png/pngsuite-all-good.html - } - - if ($gAMA) { - $gamma = 1 / $gAMA; - } - - // Don't need to apply gamma correction if == default i.e. 2.2 - if ($gamma > 2.15 && $gamma < 2.25) { - $gamma = 0; - } - - // NOT supported at present - //$j = strpos($data,'sRGB'); // sRGB colorspace - overrides gAMA - //$j = strpos($data,'cHRM'); // Chromaticity and Whitepoint - // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK - // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK - if ($firsttime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { - - // Convert to CMYK image stream - nominally returned as type='png' - $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction - if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { - $this->mpdf->PDFAXwarnings[] = sprintf('PNG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); - } - - } elseif ($firsttime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && $this->mpdf->restrictColorSpace === 1) { - - // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK - // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK - // Convert to Grayscale image stream - nominally returned as type='png' - $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction - - } elseif (($this->mpdf->PDFA || $this->mpdf->PDFX) && $pngalpha) { - - // Remove alpha channel - if ($this->mpdf->restrictColorSpace === 1) { // Grayscale - $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction - } elseif ($this->mpdf->restrictColorSpace === 3) { // CMYK - $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction - } elseif ($this->mpdf->PDFA) { // RGB - $info = $this->convertImage($data, $colspace, 'DeviceRGB', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction - } - if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { - $this->mpdf->PDFAXwarnings[] = sprintf('Transparency (alpha channel) not permitted in PDFA or PDFX files - %s - (Image converted to one without transparency.)', $file); - } - - } elseif ($firsttime && ($errpng || $pngalpha || $gamma)) { // mPDF 5.7.2 Gamma correction - - $gd = function_exists('gd_info') ? gd_info() : []; - if (!isset($gd['PNG Support'])) { - return $this->imageError($file, $firsttime, sprintf('GD library with PNG support required for image (%s)', $errpng)); - } - - $im = imagecreatefromstring($data); - if (!$im) { - return $this->imageError($file, $firsttime, sprintf('Error creating GD image from PNG file (%s)', $errpng)); - } - - $w = imagesx($im); - $h = imagesy($im); - - $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); - - // Alpha channel set (including using tRNS for Paletted images) - if ($pngalpha) { - if ($this->mpdf->PDFA) { - throw new \Mpdf\MpdfException(sprintf('PDFA1-b does not permit images with alpha channel transparency (%s).', $file)); - } - - $imgalpha = imagecreate($w, $h); - // generate gray scale pallete - for ($c = 0; $c < 256; ++$c) { - imagecolorallocate($imgalpha, $c, $c, $c); - } - - // mPDF 6 - if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS - // Read transparency info - $p = strpos($data, 'tRNS'); - if ($p) { - $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); - $transparency = substr($data, $p + 4, $n); - // ord($transparency[$index]) = the alpha value for that index - // generate alpha channel - for ($ypx = 0; $ypx < $h; ++$ypx) { - for ($xpx = 0; $xpx < $w; ++$xpx) { - $colorindex = imagecolorat($im, $xpx, $ypx); - if ($colorindex >= $n) { - $alpha = 255; - } else { - $alpha = ord($transparency[$colorindex]); - } // 0-255 - if ($alpha > 0) { - imagesetpixel($imgalpha, $xpx, $ypx, $alpha); - } - } + // Need to extract alpha channel info before imagegammacorrect (which loses the data) + if ($mask) { // i.e. $pngalpha for PNG + // mPDF 6 + if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS - only from PNG + //Read transparency info + $transparency = ''; + $p = strpos($data, 'tRNS'); + if ($p) { + $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); + $transparency = substr($data, $p + 4, $n); + // ord($transparency[$index]) = the alpha value for that index + // generate alpha channel + for ($ypx = 0; $ypx < $h; ++$ypx) { + for ($xpx = 0; $xpx < $w; ++$xpx) { + $colorindex = imagecolorat($im, $xpx, $ypx); + if ($colorindex >= $n) { + $alpha = 255; + } else { + $alpha = ord($transparency[$colorindex]); + } // 0-255 + $mimgdata .= chr($alpha); } } - } elseif ($ct === 0 || $ct === 2) { // generate Alpha channel values from tRNS - // Get transparency as array of RGB - $p = strpos($data, 'tRNS'); - if ($p) { - $trns = ''; - $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); - $t = substr($data, $p + 4, $n); - if ($colspace === 'DeviceGray') { // ct===0 - $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; - } else /* $colspace=='DeviceRGB' */ { // ct==2 - $trns = []; - $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); - $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); - $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); - } - - // generate alpha channel - for ($ypx = 0; $ypx < $h; ++$ypx) { - for ($xpx = 0; $xpx < $w; ++$xpx) { - $rgb = imagecolorat($im, $xpx, $ypx); - $r = ($rgb >> 16) & 0xFF; - $g = ($rgb >> 8) & 0xFF; - $b = $rgb & 0xFF; - if ($colspace === 'DeviceGray' && $b == $trns[0]) { - $alpha = 0; - } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { - $alpha = 0; - } else { // ct==2 - $alpha = 255; - } - if ($alpha > 0) { - imagesetpixel($imgalpha, $xpx, $ypx, $alpha); - } - } - } + } + } elseif ($pngcolortype === 0 || $pngcolortype === 2) { // generate Alpha channel values from tRNS + // Get transparency as array of RGB + $p = strpos($data, 'tRNS'); + if ($p) { + $trns = ''; + $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); + $t = substr($data, $p + 4, $n); + if ($colspace === 'DeviceGray') { // ct===0 + $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; + } else /* $colspace=='DeviceRGB' */ { // ct==2 + $trns = []; + $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); + $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); + $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); } - } else { - // extract alpha channel + + // generate alpha channel for ($ypx = 0; $ypx < $h; ++$ypx) { for ($xpx = 0; $xpx < $w; ++$xpx) { - $alpha = (imagecolorat($im, $xpx, $ypx) & 0x7F000000) >> 24; - if ($alpha < 127) { - imagesetpixel($imgalpha, $xpx, $ypx, 255 - ($alpha * 2)); + $rgb = imagecolorat($im, $xpx, $ypx); + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($colspace === 'DeviceGray' && $b == $trns[0]) { + $alpha = 0; + } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { + $alpha = 0; + } // ct==2 + else { + $alpha = 255; } + $mimgdata .= chr($alpha); } } } - - // NB This must happen after the Alpha channel is extracted - // imagegammacorrect() removes the alpha channel data in $im - (I think this is a bug in PHP) - if ($gamma) { - imagegammacorrect($im, $gamma, 2.2); - } - - $tempfile_alpha = $this->cache->tempFilename('_tempMskPNG' . md5($file) . random_int(1, 10000) . '.png'); - - $check = @imagepng($imgalpha, $tempfile_alpha); - - if (!$check) { - return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile_alpha . ') parsing PNG image with alpha channel (' . $errpng . ')'); - } - - imagedestroy($imgalpha); - // extract image without alpha channel - $imgplain = imagecreatetruecolor($w, $h); - imagealphablending($imgplain, false); // mPDF 5.7.2 - imagecopy($imgplain, $im, 0, 0, 0, 0, $w, $h); - - // create temp image file - $check = @imagepng($imgplain, $tempfile); - if (!$check) { - return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image with alpha channel (' . $errpng . ')'); - } - imagedestroy($imgplain); - // embed mask image - $minfo = $this->getImage($tempfile_alpha, false); - unlink($tempfile_alpha); - - if (!$minfo) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile_alpha . ') created with GD library to parse PNG image'); - } - - $imgmask = count($this->mpdf->images) + 1; - $minfo['cs'] = 'DeviceGray'; - $minfo['i'] = $imgmask; - $this->mpdf->images[$tempfile_alpha] = $minfo; - // embed image, masked with previously embedded mask - $info = $this->getImage($tempfile, false); - unlink($tempfile); - - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); - } - - $info['masked'] = $imgmask; - if ($ppUx) { - $info['set-dpi'] = $ppUx; - } - $info['type'] = 'png'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - } - - // No alpha/transparency set (but cannot read directly because e.g. bit-depth != 8, interlaced etc) - // ICC profile - $icc = false; - $p = strpos($data, 'iCCP'); - if ($p && $colspace === "Indexed") { // Cannot have ICC profile and Indexed together - $p += 4; - $n = $this->fourBytesToInt(substr($data, ($p - 8), 4)); - $nullsep = strpos(substr($data, $p, 80), chr(0)); - $icc = substr($data, ($p + $nullsep + 2), ($n - ($nullsep + 2))); - $icc = @gzuncompress($icc); // Ignored if fails - if ($icc) { - if (substr($icc, 36, 4) !== 'acsp') { - $icc = false; - } // invalid ICC profile - else { - $input = substr($icc, 16, 4); - $output = substr($icc, 20, 4); - // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab - if ($input !== 'RGB ' || $output !== 'XYZ ') { - $icc = false; + } else { + for ($i = 0; $i < $h; $i++) { + for ($j = 0; $j < $w; $j++) { + $rgb = imagecolorat($im, $j, $i); + $alpha = ($rgb & 0x7F000000) >> 24; + if ($alpha < 127) { + $mimgdata .= chr(255 - ($alpha * 2)); + } else { + $mimgdata .= chr(0); } } } - // Convert to RGB colorspace so can use ICC Profile - if ($icc) { - imagepalettetotruecolor($im); - $colspace = 'DeviceRGB'; - $channels = 3; - } } - - if ($gamma) { - imagegammacorrect($im, $gamma, 2.2); - } - - imagealphablending($im, false); - imagesavealpha($im, false); - imageinterlace($im, false); - - $check = @imagepng($im, $tempfile); - if (!$check) { - return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image (' . $errpng . ')'); - } - imagedestroy($im); - $info = $this->getImage($tempfile, false); - unlink($tempfile); - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); - } - - if ($ppUx) { - $info['set-dpi'] = $ppUx; - } - $info['type'] = 'png'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - if ($icc) { - $info['ch'] = $channels; - $info['icc'] = $icc; - } - $this->mpdf->images[$file] = $info; - } - - return $info; - - } else { // PNG image with no need to convert alph channels, bpc <> 8 etc. - - $parms = '/DecodeParms <>'; - //Scan chunks looking for palette, transparency and image data - $pal = ''; - $trns = ''; - $pngdata = ''; - $icc = false; - $p = 33; - - do { - $n = $this->fourBytesToInt(substr($data, $p, 4)); - $p += 4; - $type = substr($data, $p, 4); - $p += 4; - if ($type === 'PLTE') { - //Read palette - $pal = substr($data, $p, $n); - $p += $n; - $p += 4; - } elseif ($type === 'tRNS') { - //Read transparency info - $t = substr($data, $p, $n); - $p += $n; - if ($ct === 0) { - $trns = [ord(substr($t, 1, 1))]; - } elseif ($ct === 2) { - $trns = [ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))]; - } else { - $pos = strpos($t, chr(0)); - if (is_int($pos)) { - $trns = [$pos]; - } - } - $p += 4; - } elseif ($type === 'IDAT') { - $pngdata.=substr($data, $p, $n); - $p += $n; - $p += 4; - } elseif ($type === 'iCCP') { - $nullsep = strpos(substr($data, $p, 80), chr(0)); - $icc = substr($data, $p + $nullsep + 2, $n - ($nullsep + 2)); - $icc = @gzuncompress($icc); // Ignored if fails - if ($icc) { - if (substr($icc, 36, 4) !== 'acsp') { - $icc = false; - } // invalid ICC profile - else { - $input = substr($icc, 16, 4); - $output = substr($icc, 20, 4); - // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab - if ($input !== 'RGB ' || $output !== 'XYZ ') { - $icc = false; - } - } - } - $p += $n; - $p += 4; - } elseif ($type === 'IEND') { - break; - } elseif (preg_match('/[a-zA-Z]{4}/', $type)) { - $p += $n + 4; - } else { - return $this->imageError($file, $firsttime, 'Error parsing PNG image data'); - } - - } while ($n); - - if (!$pngdata) { - return $this->imageError($file, $firsttime, 'Error parsing PNG image data - no IDAT data found'); - } - - if ($colspace === 'Indexed' && empty($pal)) { - return $this->imageError($file, $firsttime, 'Error parsing PNG image data - missing colour palette'); - } - - if ($colspace === 'Indexed' && $icc) { - $icc = false; - } // mPDF 6 cannot have ICC profile and Indexed in a PDF document as both use the colorspace tag. - - $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $pngdata, 'ch' => $channels, 'icc' => $icc]; - $info['type'] = 'png'; - if ($ppUx) { - $info['set-dpi'] = $ppUx; - } - } - - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing or converting PNG image'); - } - - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - - } elseif ($type === 'gif') { // GIF - - $gd = function_exists('gd_info') - ? gd_info() - : []; - - if (isset($gd['GIF Read Support']) && $gd['GIF Read Support']) { - - $im = @imagecreatefromstring($data); - if ($im) { - $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); - imagealphablending($im, false); - imagesavealpha($im, false); - imageinterlace($im, false); - if (!is_writable($tempfile)) { - ob_start(); - $check = @imagepng($im); - if (!$check) { - return $this->imageError($file, $firsttime, 'Error creating temporary image object when using GD library to parse GIF image'); - } - $this->mpdf->imageVars['tempImage'] = ob_get_contents(); - $tempimglnk = 'var:tempImage'; - ob_end_clean(); - $info = $this->getImage($tempimglnk, false); - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file image object created with GD library to parse GIF image'); - } - imagedestroy($im); - } else { - $check = @imagepng($im, $tempfile); - if (!$check) { - return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse GIF image'); - } - $info = $this->getImage($tempfile, false); - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse GIF image'); - } - imagedestroy($im); - unlink($tempfile); - } - $info['type'] = 'gif'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - return $info; - } - - return $this->imageError($file, $firsttime, 'Error creating GD image file from GIF image'); - } - - $gif = new Gif(); - - $h = 0; - $w = 0; - - $gif->loadFile($data, 0); - - $nColors = 0; - $bgColor = -1; - $colspace = 'DeviceGray'; - $pal = ''; - - if (isset($gif->m_img->m_gih->m_bLocalClr) && $gif->m_img->m_gih->m_bLocalClr) { - $nColors = $gif->m_img->m_gih->m_nTableSize; - $pal = $gif->m_img->m_gih->m_colorTable->toString(); - if ((isset($bgColor)) && $bgColor !== -1) { // mPDF 5.7.3 - $bgColor = $gif->m_img->m_gih->m_colorTable->colorIndex($bgColor); - } - $colspace = 'Indexed'; - } elseif (isset($gif->m_gfh->m_bGlobalClr) && $gif->m_gfh->m_bGlobalClr) { - $nColors = $gif->m_gfh->m_nTableSize; - $pal = $gif->m_gfh->m_colorTable->toString(); - if ((isset($bgColor)) && $bgColor != -1) { - $bgColor = $gif->m_gfh->m_colorTable->colorIndex($bgColor); - } - $colspace = 'Indexed'; - } - - $trns = ''; - - if (isset($gif->m_img->m_bTrans) && $gif->m_img->m_bTrans && ($nColors > 0)) { - $trns = [$gif->m_img->m_nTrans]; - } - - $gifdata = $gif->m_img->m_data; - $w = $gif->m_gfh->m_nWidth; - $h = $gif->m_gfh->m_nHeight; - $gif->ClearData(); - - if ($colspace === 'Indexed' && empty($pal)) { - return $this->imageError($file, $firsttime, 'Error parsing GIF image - missing colour palette'); - } - - if ($this->mpdf->compress) { - $gifdata = $this->gzCompress($gifdata); - $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'f' => 'FlateDecode', 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; - } else { - $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; - } - - $info['type'] = 'gif'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - - } elseif ($type === 'bmp') { - - if ($this->bmp === null) { - $this->bmp = new Bmp($this->mpdf); - } - - $info = $this->bmp->_getBMPimage($data, $file); - if (isset($info['error'])) { - return $this->imageError($file, $firsttime, $info['error']); - } - - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - - } elseif ($type === 'wmf') { - - if ($this->wmf === null) { - $this->wmf = new Wmf($this->mpdf, $this->colorConverter); - } - - $wmfres = $this->wmf->_getWMFimage($data); - - if ($wmfres[0] == 0) { - if ($wmfres[1]) { - return $this->imageError($file, $firsttime, $wmfres[1]); - } - return $this->imageError($file, $firsttime, 'Error parsing WMF image'); - } - - $info = ['x' => $wmfres[2][0], 'y' => $wmfres[2][1], 'w' => $wmfres[3][0], 'h' => $wmfres[3][1], 'data' => $wmfres[1]]; - $info['i'] = count($this->mpdf->formobjects) + 1; - $info['type'] = 'wmf'; - $this->mpdf->formobjects[$file] = $info; - - return $info; - - } else { // UNKNOWN TYPE - try GD imagecreatefromstring - - $gd = function_exists('gd_info') - ? gd_info() - : []; - - if (isset($gd['PNG Support']) && $gd['PNG Support']) { - - $im = @imagecreatefromstring($data); - - if (!$im) { - return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised, and not supported by GD imagecreate'); - } - - $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); - - imagealphablending($im, false); - imagesavealpha($im, false); - imageinterlace($im, false); - - $check = @imagepng($im, $tempfile); - - if (!$check) { - return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse unknown image type'); - } - - $info = $this->getImage($tempfile, false); - - imagedestroy($im); - unlink($tempfile); - - if (!$info) { - return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse unknown image type'); - } - - $info['type'] = 'png'; - if ($firsttime) { - $info['i'] = count($this->mpdf->images) + 1; - $info['interpolation'] = $interpolation; // mPDF 6 - $this->mpdf->images[$file] = $info; - } - - return $info; - } - } - - return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised'); - } - - private function convertImage(&$data, $colspace, $targetcs, $w, $h, $dpi, $mask, $gamma_correction = false, $pngcolortype = false) - { - if (!function_exists('gd_info')) { - return $this->imageError($file, $firsttime, 'GD library needed to parse image files'); - } - - if ($this->mpdf->PDFA || $this->mpdf->PDFX) { - $mask = false; - } - - $im = @imagecreatefromstring($data); - $info = []; - $bpc = ord(substr($data, 24, 1)); - - if ($im) { - $imgdata = ''; - $mimgdata = ''; - $minfo = []; - - // mPDF 6 Gamma correction - // Need to extract alpha channel info before imagegammacorrect (which loses the data) - if ($mask) { // i.e. $pngalpha for PNG - // mPDF 6 - if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS - only from PNG - //Read transparency info - $transparency = ''; - $p = strpos($data, 'tRNS'); - if ($p) { - $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); - $transparency = substr($data, $p + 4, $n); - // ord($transparency[$index]) = the alpha value for that index - // generate alpha channel - for ($ypx = 0; $ypx < $h; ++$ypx) { - for ($xpx = 0; $xpx < $w; ++$xpx) { - $colorindex = imagecolorat($im, $xpx, $ypx); - if ($colorindex >= $n) { - $alpha = 255; - } else { - $alpha = ord($transparency[$colorindex]); - } // 0-255 - $mimgdata .= chr($alpha); - } - } - } - } elseif ($pngcolortype === 0 || $pngcolortype === 2) { // generate Alpha channel values from tRNS - // Get transparency as array of RGB - $p = strpos($data, 'tRNS'); - if ($p) { - $trns = ''; - $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); - $t = substr($data, $p + 4, $n); - if ($colspace === 'DeviceGray') { // ct===0 - $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; - } else /* $colspace=='DeviceRGB' */ { // ct==2 - $trns = []; - $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); - $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); - $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); - } - - // generate alpha channel - for ($ypx = 0; $ypx < $h; ++$ypx) { - for ($xpx = 0; $xpx < $w; ++$xpx) { - $rgb = imagecolorat($im, $xpx, $ypx); - $r = ($rgb >> 16) & 0xFF; - $g = ($rgb >> 8) & 0xFF; - $b = $rgb & 0xFF; - if ($colspace === 'DeviceGray' && $b == $trns[0]) { - $alpha = 0; - } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { - $alpha = 0; - } // ct==2 - else { - $alpha = 255; - } - $mimgdata .= chr($alpha); - } - } - } - } else { - for ($i = 0; $i < $h; $i++) { - for ($j = 0; $j < $w; $j++) { - $rgb = imagecolorat($im, $j, $i); - $alpha = ($rgb & 0x7F000000) >> 24; - if ($alpha < 127) { - $mimgdata .= chr(255 - ($alpha * 2)); - } else { - $mimgdata .= chr(0); - } - } - } - } - } + } // mPDF 6 Gamma correction if ($gamma_correction) { imagegammacorrect($im, $gamma_correction, 2.2); } - //Read transparency info + // Read transparency info $trns = []; $trnsrgb = false; if (!$this->mpdf->PDFA && !$this->mpdf->PDFX && !$mask) { // mPDF 6 added NOT mask @@ -1326,7 +470,7 @@ private function convertImage(&$data, $colspace, $targetcs, $w, $h, $dpi, $mask, $info['trns'] = $trns; } - imagedestroy($im); + $this->destroyImage($im); } return $info; } @@ -1346,6 +490,7 @@ private function jpgHeaderFromString(&$data) if ($marker !== chr(255) . chr(192) && $marker !== chr(255) . chr(194) && $marker !== chr(255) . chr(193)) { return false; } + return substr($data, $p + 2, 10); } @@ -1422,11 +567,11 @@ private function gzCompress($data) /** * Throw an exception and save re-trying image URL's which have already failed */ - private function imageError($file, $firsttime, $msg) + private function imageError($file, $firstTime, $msg) { $this->failedImages[$file] = true; - if ($firsttime && ($this->mpdf->showImageErrors || $this->mpdf->debug)) { + if ($firstTime && ($this->mpdf->showImageErrors || $this->mpdf->debug)) { throw new \Mpdf\MpdfImageException(sprintf('%s (%s)', $msg, substr($file, 0, 256))); } @@ -1453,4 +598,891 @@ private function urldecodeParts($url) return $file . $query; } + public function processJpg($data, $file, $firstTime, $interpolation) + { + $ppUx = 0; + + $hdr = $this->jpgHeaderFromString($data); + if (!$hdr) { + return $this->imageError($file, $firstTime, 'Error parsing JPG header'); + } + + $a = $this->jpgDataFromHeader($hdr); + $channels = (int) $a[4]; + $j = strpos($data, 'JFIF'); + + if ($j) { + // Read resolution + $unitSp = ord(substr($data, $j + 7, 1)); + if ($unitSp > 0) { + $ppUx = $this->twoBytesToInt(substr($data, $j + 8, 2)); // horizontal pixels per meter, usually set to zero + if ($unitSp === 2) { // = dots per cm (if == 1 set as dpi) + $ppUx = round($ppUx / 10 * 25.4); + } + } + } + + if ($a[2] === 'DeviceCMYK' && ($this->mpdf->restrictColorSpace === 2 || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace !== 3))) { + + // convert to RGB image + if (!function_exists('gd_info')) { + throw new \Mpdf\MpdfException(sprintf('JPG image may not use CMYK color space (%s).', $file)); + } + + if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) { + $this->mpdf->PDFAXwarnings[] = sprintf('JPG image "%s" may not use CMYK color space. Image converted to RGB. The colour profile was altered', $file); + } + + $im = @imagecreatefromstring($data); + + if ($im) { + + $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); + imageinterlace($im, false); + + $check = @imagepng($im, $tempfile); + if (!$check) { + return $this->imageError($file, $firstTime, sprintf('Error creating temporary file "%s" when using GD library to parse JPG (CMYK) image', $tempfile)); + } + + // $info = $this->getImage($tempfile, false); + + $data = file_get_contents($tempfile); + $info = $this->processPng($data, $tempfile, false, $interpolation); + + if (!$info) { + return $this->imageError($file, $firstTime, sprintf('Error parsing temporary file "%s" created with GD library to parse JPG (CMYK) image', $tempfile)); + } + + $this->destroyImage($im); + unlink($tempfile); + + $info['type'] = 'jpg'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + return $this->imageError($file, $firstTime, 'Error creating GD image file from JPG(CMYK) image'); + } + + if ($a[2] === 'DeviceRGB' && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { + // Convert to CMYK image stream - nominally returned as type='png' + $info = $this->convertImage($data, $a[2], 'DeviceCMYK', $a[0], $a[1], $ppUx, false); + if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { + $this->mpdf->PDFAXwarnings[] = sprintf('JPG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); + } + + } elseif (($a[2] === 'DeviceRGB' || $a[2] === 'DeviceCMYK') && $this->mpdf->restrictColorSpace === 1) { + // Convert to Grayscale image stream - nominally returned as type='png' + $info = $this->convertImage($data, $a[2], 'DeviceGray', $a[0], $a[1], $ppUx, false); + + } else { + // mPDF 6 Detect Adobe APP14 Tag + //$pos = strpos($data, "\xFF\xEE\x00\x0EAdobe\0"); + //if ($pos !== false) { + //} + // mPDF 6 ICC profile + $offset = 0; + $icc = []; + while (($pos = strpos($data, "ICC_PROFILE\0", $offset)) !== false) { + // get ICC sequence length + $length = $this->twoBytesToInt(substr($data, $pos - 2, 2)) - 16; + $sn = max(1, ord($data[$pos + 12])); + $nom = max(1, ord($data[$pos + 13])); + $icc[$sn - 1] = substr($data, $pos + 14, $length); + $offset = ($pos + 14 + $length); + } + // order and compact ICC segments + if (count($icc) > 0) { + ksort($icc); + $icc = implode('', $icc); + if (substr($icc, 36, 4) !== 'acsp') { + // invalid ICC profile + $icc = false; + } + $input = substr($icc, 16, 4); + $output = substr($icc, 20, 4); + // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab + if ($input !== 'RGB ' || $output !== 'XYZ ') { + $icc = false; + } + } else { + $icc = false; + } + + $info = ['w' => $a[0], 'h' => $a[1], 'cs' => $a[2], 'bpc' => $a[3], 'f' => 'DCTDecode', 'data' => $data, 'type' => 'jpg', 'ch' => $channels, 'icc' => $icc]; + if ($ppUx) { + $info['set-dpi'] = $ppUx; + } + } + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing or converting JPG image'); + } + + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + public function processPng($data, $file, $firstTime, $interpolation) + { + $ppUx = 0; + + // Check signature + if (strpos($data, chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) !== 0) { + return $this->imageError($file, $firstTime, 'Error parsing PNG identifier'); + } + + // Read header chunk + if (substr($data, 12, 4) !== 'IHDR') { + return $this->imageError($file, $firstTime, 'Incorrect PNG file (no IHDR block found)'); + } + + $w = $this->fourBytesToInt(substr($data, 16, 4)); + $h = $this->fourBytesToInt(substr($data, 20, 4)); + $bpc = ord(substr($data, 24, 1)); + $errpng = false; + $pngalpha = false; + $channels = 0; + + // if($bpc>8) { $errpng = 'not 8-bit depth'; } // mPDF 6 Allow through to be handled as native PNG + $ct = ord(substr($data, 25, 1)); + + if ($ct === 0) { + $colspace = 'DeviceGray'; + $channels = 1; + } elseif ($ct === 2) { + $colspace = 'DeviceRGB'; + $channels = 3; + } elseif ($ct === 3) { + $colspace = 'Indexed'; + $channels = 1; + } elseif ($ct === 4) { + $colspace = 'DeviceGray'; + $channels = 1; + $errpng = 'alpha channel'; + $pngalpha = true; + } else { + $colspace = 'DeviceRGB'; + $channels = 3; + $errpng = 'alpha channel'; + $pngalpha = true; + } + + if ($ct < 4 && strpos($data, 'tRNS') !== false) { + $errpng = 'transparency'; + $pngalpha = true; + } // mPDF 6 + + if ($ct === 3 && strpos($data, 'iCCP') !== false) { + $errpng = 'indexed plus ICC'; + } // mPDF 6 + + // $pngalpha is used as a FLAG of any kind of transparency which COULD be tranferred to an alpha channel + // incl. single-color tarnsparency, depending which type of handling occurs later + if (ord(substr($data, 26, 1)) !== 0) { + $errpng = 'compression method'; + } // only 0 should be specified + + if (ord(substr($data, 27, 1)) !== 0) { + $errpng = 'filter method'; + } // only 0 should be specified + + if (ord(substr($data, 28, 1)) !== 0) { + $errpng = 'interlaced file'; + } + + $j = strpos($data, 'pHYs'); + if ($j) { + //Read resolution + $unitSp = ord(substr($data, $j + 12, 1)); + if ($unitSp === 1) { + $ppUx = $this->fourBytesToInt(substr($data, $j + 4, 4)); // horizontal pixels per meter, usually set to zero + $ppUx = round($ppUx / 1000 * 25.4); + } + } + + // mPDF 6 Gamma correction + $gamma = 0; + $gAMA = 0; + $j = strpos($data, 'gAMA'); + if ($j && strpos($data, 'sRGB') === false) { // sRGB colorspace - overrides gAMA + $gAMA = $this->fourBytesToInt(substr($data, $j + 4, 4)); // Gamma value times 100000 + $gAMA /= 100000; + + // http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html + // "If the source file's gamma value is greater than 1.0, it is probably a display system exponent,..." + // ("..and you should use its reciprocal for the PNG gamma.") + //if ($gAMA > 1) { $gAMA = 1/$gAMA; } + // (Some) Applications seem to ignore it... appearing how it was probably intended + // Test Case - image(s) on http://www.w3.org/TR/CSS21/intro.html - PNG has gAMA set as 1.45454 + // Probably unintentional as mentioned above and should be 0.45454 which is 1 / 2.2 + // Tested on Windows PC + // Firefox and Opera display gray as 234 (correct, but looks wrong) + // IE9 and Safari display gray as 193 (incorrect but looks right) + // See test different gamma chunks at http://www.libpng.org/pub/png/pngsuite-all-good.html + } + + if ($gAMA) { + $gamma = 1 / $gAMA; + } + + // Don't need to apply gamma correction if == default i.e. 2.2 + if ($gamma > 2.15 && $gamma < 2.25) { + $gamma = 0; + } + + // NOT supported at present + //$j = strpos($data,'sRGB'); // sRGB colorspace - overrides gAMA + //$j = strpos($data,'cHRM'); // Chromaticity and Whitepoint + // $firstTime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK + // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK + if ($firstTime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { + + // Convert to CMYK image stream - nominally returned as type='png' + $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction + if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { + $this->mpdf->PDFAXwarnings[] = sprintf('PNG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); + } + + } elseif ($firstTime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && $this->mpdf->restrictColorSpace === 1) { + + // $firstTime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK + // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK + // Convert to Grayscale image stream - nominally returned as type='png' + $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction + + } elseif (($this->mpdf->PDFA || $this->mpdf->PDFX) && $pngalpha) { + + // Remove alpha channel + if ($this->mpdf->restrictColorSpace === 1) { // Grayscale + $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction + } elseif ($this->mpdf->restrictColorSpace === 3) { // CMYK + $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction + } elseif ($this->mpdf->PDFA) { // RGB + $info = $this->convertImage($data, $colspace, 'DeviceRGB', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction + } + if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { + $this->mpdf->PDFAXwarnings[] = sprintf('Transparency (alpha channel) not permitted in PDFA or PDFX files - %s - (Image converted to one without transparency.)', $file); + } + + } elseif ($firstTime && ($errpng || $pngalpha || $gamma)) { // mPDF 5.7.2 Gamma correction + + $gd = function_exists('gd_info') ? gd_info() : []; + if (!isset($gd['PNG Support'])) { + return $this->imageError($file, $firstTime, sprintf('GD library with PNG support required for image (%s)', $errpng)); + } + + $im = @imagecreatefromstring($data); + if (!$im) { + return $this->imageError($file, $firstTime, sprintf('Error creating GD image from PNG file (%s)', $errpng)); + } + + $w = imagesx($im); + $h = imagesy($im); + + $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . bin2hex(random_bytes(6)) . '.png'); + + // Alpha channel set (including using tRNS for Paletted images) + if ($pngalpha) { + if ($this->mpdf->PDFA) { + throw new \Mpdf\MpdfException(sprintf('PDFA1-b does not permit images with alpha channel transparency (%s).', $file)); + } + + $imgalpha = imagecreate($w, $h); + // generate gray scale pallete + for ($c = 0; $c < 256; ++$c) { + imagecolorallocate($imgalpha, $c, $c, $c); + } + + // mPDF 6 + if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS + // Read transparency info + $p = strpos($data, 'tRNS'); + if ($p) { + $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); + $transparency = substr($data, $p + 4, $n); + // ord($transparency[$index]) = the alpha value for that index + // generate alpha channel + for ($ypx = 0; $ypx < $h; ++$ypx) { + for ($xpx = 0; $xpx < $w; ++$xpx) { + $colorindex = imagecolorat($im, $xpx, $ypx); + if ($colorindex >= $n) { + $alpha = 255; + } else { + $alpha = ord($transparency[$colorindex]); + } // 0-255 + if ($alpha > 0) { + imagesetpixel($imgalpha, $xpx, $ypx, $alpha); + } + } + } + } + } elseif ($ct === 0 || $ct === 2) { // generate Alpha channel values from tRNS + // Get transparency as array of RGB + $p = strpos($data, 'tRNS'); + if ($p) { + $trns = ''; + $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); + $t = substr($data, $p + 4, $n); + if ($colspace === 'DeviceGray') { // ct===0 + $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; + } else /* $colspace=='DeviceRGB' */ { // ct==2 + $trns = []; + $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); + $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); + $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); + } + + // generate alpha channel + for ($ypx = 0; $ypx < $h; ++$ypx) { + for ($xpx = 0; $xpx < $w; ++$xpx) { + $rgb = imagecolorat($im, $xpx, $ypx); + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + if ($colspace === 'DeviceGray' && $b == $trns[0]) { + $alpha = 0; + } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { + $alpha = 0; + } else { // ct==2 + $alpha = 255; + } + if ($alpha > 0) { + imagesetpixel($imgalpha, $xpx, $ypx, $alpha); + } + } + } + } + } else { + // extract alpha channel + for ($ypx = 0; $ypx < $h; ++$ypx) { + for ($xpx = 0; $xpx < $w; ++$xpx) { + $alpha = (imagecolorat($im, $xpx, $ypx) & 0x7F000000) >> 24; + if ($alpha < 127) { + imagesetpixel($imgalpha, $xpx, $ypx, 255 - ($alpha * 2)); + } + } + } + } + + // NB This must happen after the Alpha channel is extracted + // imagegammacorrect() removes the alpha channel data in $im - (I think this is a bug in PHP) + if ($gamma) { + imagegammacorrect($im, $gamma, 2.2); + } + + $tempfile_alpha = $this->cache->tempFilename('_tempMskPNG' . md5($file) . random_int(1, 10000) . '.png'); + + $check = @imagepng($imgalpha, $tempfile_alpha); + + if (!$check) { + return $this->imageError($file, $firstTime, 'Failed to create temporary image file (' . $tempfile_alpha . ') parsing PNG image with alpha channel (' . $errpng . ')'); + } + + $this->destroyImage($imgalpha); + // extract image without alpha channel + $imgplain = imagecreatetruecolor($w, $h); + imagealphablending($imgplain, false); // mPDF 5.7.2 + imagecopy($imgplain, $im, 0, 0, 0, 0, $w, $h); + + // create temp image file + $check = @imagepng($imgplain, $tempfile); + if (!$check) { + return $this->imageError($file, $firstTime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image with alpha channel (' . $errpng . ')'); + } + + $this->destroyImage($imgplain); + + // embed mask image + //$minfo = $this->getImage($tempfile_alpha, false); + $data = file_get_contents($tempfile_alpha); + $minfo = $this->processPng($data, $tempfile_alpha, false, $interpolation); + + unlink($tempfile_alpha); + + if (!$minfo) { + return $this->imageError($file, $firstTime, 'Error parsing temporary file (' . $tempfile_alpha . ') created with GD library to parse PNG image'); + } + + $imgmask = count($this->mpdf->images) + 1; + $minfo['cs'] = 'DeviceGray'; + $minfo['i'] = $imgmask; + $this->mpdf->images[$tempfile_alpha] = $minfo; + // embed image, masked with previously embedded mask + + // $info = $this->getImage($tempfile, false); + $data = file_get_contents($tempfile); + $info = $this->processPng($data, $tempfile, false, $interpolation); + + unlink($tempfile); + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); + } + + $info['masked'] = $imgmask; + if ($ppUx) { + $info['set-dpi'] = $ppUx; + } + $info['type'] = 'png'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + // No alpha/transparency set (but cannot read directly because e.g. bit-depth != 8, interlaced etc) + // ICC profile + $icc = false; + $p = strpos($data, 'iCCP'); + if ($p && $colspace === "Indexed") { // Cannot have ICC profile and Indexed together + $p += 4; + $n = $this->fourBytesToInt(substr($data, ($p - 8), 4)); + $nullsep = strpos(substr($data, $p, 80), chr(0)); + $icc = substr($data, ($p + $nullsep + 2), ($n - ($nullsep + 2))); + $icc = @gzuncompress($icc); // Ignored if fails + if ($icc) { + if (substr($icc, 36, 4) !== 'acsp') { + $icc = false; + } // invalid ICC profile + else { + $input = substr($icc, 16, 4); + $output = substr($icc, 20, 4); + // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab + if ($input !== 'RGB ' || $output !== 'XYZ ') { + $icc = false; + } + } + } + // Convert to RGB colorspace so can use ICC Profile + if ($icc) { + imagepalettetotruecolor($im); + $colspace = 'DeviceRGB'; + $channels = 3; + } + } + + if ($gamma) { + imagegammacorrect($im, $gamma, 2.2); + } + + imagealphablending($im, false); + imagesavealpha($im, false); + imageinterlace($im, false); + + $check = @imagepng($im, $tempfile); + if (!$check) { + return $this->imageError($file, $firstTime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image (' . $errpng . ')'); + } + + $this->destroyImage($im); + // $info = $this->getImage($tempfile, false); + $data = file_get_contents($tempfile); + $info = $this->processPng($data, $tempfile, false, $interpolation); + unlink($tempfile); + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); + } + + if ($ppUx) { + $info['set-dpi'] = $ppUx; + } + $info['type'] = 'png'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + if ($icc) { + $info['ch'] = $channels; + $info['icc'] = $icc; + } + $this->mpdf->images[$file] = $info; + } + + return $info; + + } else { // PNG image with no need to convert alph channels, bpc <> 8 etc. + + $parms = '/DecodeParms <>'; + //Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $pngdata = ''; + $icc = false; + $p = 33; + + do { + $n = $this->fourBytesToInt(substr($data, $p, 4)); + $p += 4; + $type = substr($data, $p, 4); + $p += 4; + if ($type === 'PLTE') { + //Read palette + $pal = substr($data, $p, $n); + $p += $n; + $p += 4; + } elseif ($type === 'tRNS') { + //Read transparency info + $t = substr($data, $p, $n); + $p += $n; + if ($ct === 0) { + $trns = [ord(substr($t, 1, 1))]; + } elseif ($ct === 2) { + $trns = [ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))]; + } else { + $pos = strpos($t, chr(0)); + if (is_int($pos)) { + $trns = [$pos]; + } + } + $p += 4; + } elseif ($type === 'IDAT') { + $pngdata.=substr($data, $p, $n); + $p += $n; + $p += 4; + } elseif ($type === 'iCCP') { + $nullsep = strpos(substr($data, $p, 80), chr(0)); + $icc = substr($data, $p + $nullsep + 2, $n - ($nullsep + 2)); + $icc = @gzuncompress($icc); // Ignored if fails + if ($icc) { + if (substr($icc, 36, 4) !== 'acsp') { + $icc = false; + } // invalid ICC profile + else { + $input = substr($icc, 16, 4); + $output = substr($icc, 20, 4); + // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab + if ($input !== 'RGB ' || $output !== 'XYZ ') { + $icc = false; + } + } + } + $p += $n; + $p += 4; + } elseif ($type === 'IEND') { + break; + } elseif (preg_match('/[a-zA-Z]{4}/', $type)) { + $p += $n + 4; + } else { + return $this->imageError($file, $firstTime, 'Error parsing PNG image data'); + } + + } while ($n); + + if (!$pngdata) { + return $this->imageError($file, $firstTime, 'Error parsing PNG image data - no IDAT data found'); + } + + if ($colspace === 'Indexed' && empty($pal)) { + return $this->imageError($file, $firstTime, 'Error parsing PNG image data - missing colour palette'); + } + + // mPDF 6 cannot have ICC profile and Indexed in a PDF document as both use the colorspace tag. + if ($colspace === 'Indexed' && $icc) { + $icc = false; + } + + $info = [ + 'w' => $w, + 'h' => $h, + 'cs' => $colspace, + 'bpc' => $bpc, + 'f' => 'FlateDecode', + 'parms' => $parms, + 'pal' => $pal, + 'trns' => $trns, + 'data' => $pngdata, + 'ch' => $channels, + 'icc' => $icc + ]; + + $info['type'] = 'png'; + + if ($ppUx) { + $info['set-dpi'] = $ppUx; + } + } + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing or converting PNG image'); + } + + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + public function processWebp($data, $file, $firstTime) + { + $im = @imagecreatefromstring($data); + + if (!function_exists('imagewebp') || false === $im) { + return $this->imageError($file, $firstTime, 'Missing GD support for WEBP images.'); + } + + $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); + $checkfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); + + $check = imagewebp($im, $checkfile); + if (!$check) { + return $this->imageError($file, $firstTime, sprintf('Error creating temporary file "%s" when using GD library to parse WEBP image', $checkfile)); + } + + @imagejpeg($im, $tempfile); + $data = file_get_contents($tempfile); + $this->destroyImage($im); + unlink($tempfile); + unlink($checkfile); + + return $data; + } + + public function processSvg($data, $file, $firstTime) + { + $svg = new Svg($this->mpdf, $this->otl, $this->cssManager, $this, $this->sizeConverter, $this->colorConverter, $this->languageToFont, $this->scriptToLanguage); + + $family = $this->mpdf->FontFamily; + $style = $this->mpdf->FontStyle; + $size = $this->mpdf->FontSizePt; + + $info = $svg->ImageSVG($data); + + // Restore font + if ($family) { + $this->mpdf->SetFont($family, $style, $size, false); + } + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing SVG file'); + } + + $info['type'] = 'svg'; + $info['i'] = count($this->mpdf->formobjects) + 1; + $this->mpdf->formobjects[$file] = $info; + + return $info; + } + + public function processGif($data, $file, $firstTime, $interpolation) + { + $gd = function_exists('gd_info') + ? gd_info() + : []; + + if (isset($gd['GIF Read Support']) && $gd['GIF Read Support']) { + + $im = @imagecreatefromstring($data); + + if ($im) { + + $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); + + imagealphablending($im, false); + imagesavealpha($im, false); + imageinterlace($im, false); + + $check = @imagepng($im, $tempfile); + if (!$check) { + return $this->imageError($file, $firstTime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse GIF image'); + } + + // $info = $this->getImage($tempfile, false); + $data = file_get_contents($tempfile); + $info = $this->processPng($data, $tempfile, false, $interpolation); + + if (!$info) { + return $this->imageError($file, $firstTime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse GIF image'); + } + + $this->destroyImage($im); + unlink($tempfile); + + $info['type'] = 'gif'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + return $info; + } + + return $this->imageError($file, $firstTime, 'Error creating GD image file from GIF image'); + } + + $gif = new Gif(); + + $h = 0; + $w = 0; + + $gif->loadFile($data, 0); + + $nColors = 0; + $bgColor = -1; + $colspace = 'DeviceGray'; + $pal = ''; + + if (isset($gif->m_img->m_gih->m_bLocalClr) && $gif->m_img->m_gih->m_bLocalClr) { + $nColors = $gif->m_img->m_gih->m_nTableSize; + $pal = $gif->m_img->m_gih->m_colorTable->toString(); + if ((isset($bgColor)) && $bgColor !== -1) { // mPDF 5.7.3 + $bgColor = $gif->m_img->m_gih->m_colorTable->colorIndex($bgColor); + } + $colspace = 'Indexed'; + } elseif (isset($gif->m_gfh->m_bGlobalClr) && $gif->m_gfh->m_bGlobalClr) { + $nColors = $gif->m_gfh->m_nTableSize; + $pal = $gif->m_gfh->m_colorTable->toString(); + if ((isset($bgColor)) && $bgColor != -1) { + $bgColor = $gif->m_gfh->m_colorTable->colorIndex($bgColor); + } + $colspace = 'Indexed'; + } + + $trns = ''; + + if (isset($gif->m_img->m_bTrans) && $gif->m_img->m_bTrans && ($nColors > 0)) { + $trns = [$gif->m_img->m_nTrans]; + } + + $gifdata = $gif->m_img->m_data; + $w = $gif->m_gfh->m_nWidth; + $h = $gif->m_gfh->m_nHeight; + $gif->ClearData(); + + if ($colspace === 'Indexed' && empty($pal)) { + return $this->imageError($file, $firstTime, 'Error parsing GIF image - missing colour palette'); + } + + if ($this->mpdf->compress) { + $gifdata = $this->gzCompress($gifdata); + $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'f' => 'FlateDecode', 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; + } else { + $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; + } + + $info['type'] = 'gif'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + public function processBmp($data, $file, $firstTime, $interpolation) + { + if ($this->bmp === null) { + $this->bmp = new Bmp($this->mpdf); + } + + $info = $this->bmp->_getBMPimage($data, $file); + if (isset($info['error'])) { + return $this->imageError($file, $firstTime, $info['error']); + } + + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + + public function processWmf($data, $file, $firstTime) + { + if ($this->wmf === null) { + $this->wmf = new Wmf($this->mpdf, $this->colorConverter); + } + + $wmfres = $this->wmf->_getWMFimage($data); + + if ($wmfres[0] == 0) { + if ($wmfres[1]) { + return $this->imageError($file, $firstTime, $wmfres[1]); + } + return $this->imageError($file, $firstTime, 'Error parsing WMF image'); + } + + $info = ['x' => $wmfres[2][0], 'y' => $wmfres[2][1], 'w' => $wmfres[3][0], 'h' => $wmfres[3][1], 'data' => $wmfres[1]]; + $info['i'] = count($this->mpdf->formobjects) + 1; + $info['type'] = 'wmf'; + $this->mpdf->formobjects[$file] = $info; + + return $info; + } + + public function processUnknownType($data, $file, $firstTime, $interpolation) + { + $gd = function_exists('gd_info') + ? gd_info() + : []; + + if (isset($gd['PNG Support']) && $gd['PNG Support']) { + + $im = @imagecreatefromstring($data); + + if (!$im) { + return $this->imageError($file, $firstTime, 'Error parsing image file - image type not recognised and/or not supported by GD imagecreate'); + } + + $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); + + imagealphablending($im, false); + imagesavealpha($im, false); + imageinterlace($im, false); + + $check = @imagepng($im, $tempfile); + + if (!$check) { + return $this->imageError($file, $firstTime, sprintf('Error creating temporary file "%s" when using GD library to parse unknown image type', $tempfile)); + } + + //$info = $this->getImage($tempfile, false); + $data = file_get_contents($tempfile); + $info = $this->processPng($data, $tempfile, false, $interpolation); + + $this->destroyImage($im); + unlink($tempfile); + + if (!$info) { + return $this->imageError($file, $firstTime, sprintf('Error parsing temporary file "%s" created with GD library to parse unknown image type', $tempfile)); + } + + $info['type'] = 'png'; + if ($firstTime) { + $info['i'] = count($this->mpdf->images) + 1; + $info['interpolation'] = $interpolation; // mPDF 6 + $this->mpdf->images[$file] = $info; + } + + return $info; + } + } + + private function destroyImage($im) + { + if (PHP_VERSION_ID < 80000) { + imagedestroy($im); + } + } + } diff --git a/vendor/mpdf/mpdf/src/Image/Svg.php b/vendor/mpdf/mpdf/src/Image/Svg.php index 9bc0f607..602370ae 100644 --- a/vendor/mpdf/mpdf/src/Image/Svg.php +++ b/vendor/mpdf/mpdf/src/Image/Svg.php @@ -1040,13 +1040,13 @@ function svgOffset($attribs) // save all tag attributes $this->svg_attribs = $attribs; if (isset($this->svg_attribs['viewBox'])) { - $vb = preg_split('/\s+/is', trim($this->svg_attribs['viewBox'])); + $vb = preg_split('/[\s,]+/is', trim($this->svg_attribs['viewBox'])); if (count($vb) == 4) { $this->svg_info['x'] = $vb[0]; $this->svg_info['y'] = $vb[1]; $this->svg_info['w'] = $vb[2]; $this->svg_info['h'] = $vb[3]; -// return; + // return; } } $svg_w = 0; @@ -1058,8 +1058,6 @@ function svgOffset($attribs) $svg_h = $this->sizeConverter->convert($attribs['height']); // mm } - -///* // mPDF 5.0.005 if (isset($this->svg_info['w']) && $this->svg_info['w']) { // if 'w' set by viewBox if ($svg_w) { // if width also set, use these values to determine to set size of "pixel" @@ -1071,7 +1069,7 @@ function svgOffset($attribs) } return; } -//*/ + // Added to handle file without height or width specified if (!$svg_w && !$svg_h) { $svg_w = $svg_h = $this->mpdf->blk[$this->mpdf->blklvl]['inner_width']; @@ -1481,11 +1479,11 @@ function svgStyle($critere_style, $attribs, $element) for ($i = 0; $i < count($d); $i += 2) { - if ($d[$i] === 'none') { + if ($d[$i] === '' || $d[$i] === 'none') { continue; } - $arr .= sprintf('%.3F %.3F ', $d[$i] * $this->kp, $d[$i + 1] * $this->kp); + $arr .= sprintf('%.3F %.3F ', (float) $d[$i] * $this->kp, (float) $d[$i + 1] * $this->kp); } if (isset($critere_style['stroke-dashoffset'])) { diff --git a/vendor/mpdf/mpdf/src/Mpdf.php b/vendor/mpdf/mpdf/src/Mpdf.php index a54b8290..b4403035 100644 --- a/vendor/mpdf/mpdf/src/Mpdf.php +++ b/vendor/mpdf/mpdf/src/Mpdf.php @@ -10,11 +10,11 @@ use Mpdf\Log\Context as LogContext; use Mpdf\Fonts\MetricsGenerator; use Mpdf\Output\Destination; +use Mpdf\PsrLogAwareTrait\MpdfPsrLogAwareTrait; use Mpdf\QrCode; use Mpdf\Utils\Arrays; use Mpdf\Utils\NumericString; use Mpdf\Utils\UtfString; -use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; /** @@ -30,11 +30,14 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface use Strict; use FpdiTrait; + use MpdfPsrLogAwareTrait; - const VERSION = '8.0.17'; + const VERSION = '8.2.7'; const SCALE = 72 / 25.4; + const OBJECT_IDENTIFIER = "\xbb\xa4\xac"; + var $useFixedNormalLineHeight; // mPDF 6 var $useFixedTextBaseline; // mPDF 6 var $adjustFontDescLineheight; // mPDF 6 @@ -73,7 +76,7 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface var $PDFXauto; var $PDFA; - var $PDFAversion = '1-B'; + var $PDFAversion; var $PDFAauto; var $ICCProfile; @@ -830,6 +833,9 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface private $preambleWritten = false; + private $watermarkTextObject; + private $watermarkImageObject; + /** * @var string */ @@ -937,9 +943,19 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface private $protection; /** - * @var \Mpdf\RemoteContentFetcher + * @var \Mpdf\Http\ClientInterface + */ + private $httpClient; + + /** + * @var \Mpdf\File\LocalContentLoaderInterface + */ + private $localContentLoader; + + /** + * @var \Mpdf\AssetFetcher */ - private $remoteContentFetcher; + private $assetFetcher; /** * @var \Mpdf\Image\ImageProcessor @@ -956,11 +972,6 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface */ private $scriptToLanguage; - /** - * @var \Psr\Log\LoggerInterface - */ - private $logger; - /** * @var \Mpdf\Writer\BaseWriter */ @@ -1026,13 +1037,21 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface */ private $services; + /** + * @var \Mpdf\Container\ContainerInterface + */ + private $container; + /** * @param mixed[] $config + * @param \Mpdf\Container\ContainerInterface|null $container Experimental container to override internal services */ - public function __construct(array $config = []) + public function __construct(array $config = [], $container = null) { $this->_dochecks(); + assert(!$container || $container instanceof \Mpdf\Container\ContainerInterface); + list( $mode, $format, @@ -1052,12 +1071,11 @@ public function __construct(array $config = []) $originalConfig = $config; $config = $this->initConfig($originalConfig); - $serviceFactory = new ServiceFactory(); + $serviceFactory = new ServiceFactory($container); $services = $serviceFactory->getServices( $this, $this->logger, $config, - $this->restrictColorSpace, $this->languageToFont, $this->scriptToLanguage, $this->fontDescriptor, @@ -1066,6 +1084,7 @@ public function __construct(array $config = []) $this->wmf ); + $this->container = $container; $this->services = []; foreach ($services as $key => $service) { @@ -1206,7 +1225,7 @@ public function __construct(array $config = []) $this->breakpoints = []; // used in columnbuffer $this->tableLevel = 0; $this->tbctr = []; // counter for nested tables at each level - $this->page_box = []; + $this->page_box = new PageBox(); $this->show_marks = ''; // crop or cross marks $this->kwt = false; $this->kwt_height = 0; @@ -1561,24 +1580,6 @@ public function cleanup() $this->createdReaders = []; } - /** - * @param \Psr\Log\LoggerInterface - * - * @return \Mpdf\Mpdf - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - - foreach ($this->services as $name) { - if ($this->$name && $this->$name instanceof \Psr\Log\LoggerAwareInterface) { - $this->$name->setLogger($logger); - } - } - - return $this; - } - private function initConfig(array $config) { $configObject = new ConfigVariables(); @@ -1638,11 +1639,9 @@ function _setPageSize($format, &$orientation) } // e.g. A4-L = A4 landscape, A4-P = A4 portrait + $orientation = $orientation ?: 'P'; if (preg_match('/([0-9a-zA-Z]*)-([P,L])/i', $format, $m)) { - $format = $m[1]; - $orientation = $m[2]; - } elseif (empty($orientation)) { - $orientation = 'P'; + list(, $format, $orientation) = $m; } $format = PageFormat::getSizeFromName($format); @@ -1665,11 +1664,11 @@ function _setPageSize($format, &$orientation) // Page orientation $orientation = strtolower($orientation); - if ($orientation === 'p' || $orientation == 'portrait') { + if ($orientation === 'p' || $orientation === 'portrait') { $orientation = 'P'; $this->wPt = $this->fwPt; $this->hPt = $this->fhPt; - } elseif ($orientation === 'l' || $orientation == 'landscape') { + } elseif ($orientation === 'l' || $orientation === 'landscape') { $orientation = 'L'; $this->wPt = $this->fhPt; $this->hPt = $this->fwPt; @@ -1914,6 +1913,11 @@ function SetAlpha($alpha, $bm = 'Normal', $return = false, $mode = 'B') } } + /** + * @param mixed[] $parms + * + * @return int + */ function AddExtGState($parms) { $n = count($this->extgstates); @@ -1934,6 +1938,7 @@ function AddExtGState($parms) } $n++; $this->extgstates[$n]['parms'] = $parms; + return $n; } @@ -3913,6 +3918,7 @@ function AddFont($family, $style = '') $regenerate = true; } // mPDF 6 + $glyphIDtoUni = null; if (empty($font['name']) || $font['originalsize'] != $ttfstat['size'] || $regenerate) { $generator = new MetricsGenerator($this->fontCache, $this->fontDescriptor); @@ -5417,26 +5423,15 @@ function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) { - // Generate PDF string - // ============================== - if ((isset($this->CurrentFont['sip']) && $this->CurrentFont['sip']) || (isset($this->CurrentFont['smp']) && $this->CurrentFont['smp'])) { - $sipset = true; - } else { - $sipset = false; - } + $sipset = (isset($this->CurrentFont['sip']) && $this->CurrentFont['sip']) + || (isset($this->CurrentFont['smp']) && $this->CurrentFont['smp']); - if ($textvar & TextVars::FC_SMALLCAPS) { - $smcaps = true; - } // IF SmallCaps using transformation, NOT OTL - else { - $smcaps = false; - } + $smcaps = ($textvar & TextVars::FC_SMALLCAPS); + + $fontid = $sipset + ? $last_fontid = $original_fontid = $this->CurrentFont['subsetfontids'][0] + : $last_fontid = $original_fontid = $this->CurrentFont['i']; - if ($sipset) { - $fontid = $last_fontid = $original_fontid = $this->CurrentFont['subsetfontids'][0]; - } else { - $fontid = $last_fontid = $original_fontid = $this->CurrentFont['i']; - } $SmallCapsON = false; // state: uppercase/not $lastSmallCapsON = false; // state: uppercase/not $last_fontsize = $fontsize = $this->FontSizePt; @@ -5453,14 +5448,9 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) $XshiftAfter = 0; $lastYPlacement = 0; - if ($sipset) { - // mPDF 6 DELETED ******** - // $txt= preg_replace('/'.preg_quote($this->aliasNbPg,'/').'/', chr(7), $txt); // ? Need to adjust OTL info - // $txt= preg_replace('/'.preg_quote($this->aliasNbPgGp,'/').'/', chr(8), $txt); // ? Need to adjust OTL info - $tj = '<'; - } else { - $tj = '('; - } + $tj = $sipset + ? '<' + : '('; for ($i = 0; $i < count($unicode); $i++) { $c = $unicode[$i]; @@ -5478,7 +5468,7 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) } // XPlacement from GPOS if (isset($GPOSinfo[$i]['XPlacement']) && $GPOSinfo[$i]['XPlacement']) { - if (!isset($GPOSinfo[$i]['wDir']) || $GPOSinfo[$i]['wDir'] != 'RTL') { + if (!isset($GPOSinfo[$i]['wDir']) || $GPOSinfo[$i]['wDir'] !== 'RTL') { if (isset($GPOSinfo[$i]['BaseWidth'])) { $GPOSinfo[$i]['XPlacement'] -= $GPOSinfo[$i]['BaseWidth']; } @@ -5500,19 +5490,19 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) $XshiftAfter += $wordspacing; } - if (substr($OTLdata['group'], ($i + 1), 1) != 'M') { // Don't add inter-character spacing before Marks + if (substr($OTLdata['group'], ($i + 1), 1) !== 'M') { // Don't add inter-character spacing before Marks $XshiftAfter += $charspacing; } // ...applyGPOSpdf... // XAdvance from GPOS - Convert to PDF Text space (thousandths of a unit ); - if (((isset($GPOSinfo[$i]['wDir']) && $GPOSinfo[$i]['wDir'] != 'RTL') || !isset($GPOSinfo[$i]['wDir'])) && isset($GPOSinfo[$i]['XAdvanceL']) && $GPOSinfo[$i]['XAdvanceL']) { + if (((isset($GPOSinfo[$i]['wDir']) && $GPOSinfo[$i]['wDir'] !== 'RTL') || !isset($GPOSinfo[$i]['wDir'])) && isset($GPOSinfo[$i]['XAdvanceL']) && $GPOSinfo[$i]['XAdvanceL']) { $XshiftAfter += $GPOSinfo[$i]['XAdvanceL'] * 1000 / $this->CurrentFont['unitsPerEm']; - } elseif (isset($GPOSinfo[$i]['wDir']) && $GPOSinfo[$i]['wDir'] == 'RTL' && isset($GPOSinfo[$i]['XAdvanceR']) && $GPOSinfo[$i]['XAdvanceR']) { + } elseif (isset($GPOSinfo[$i]['wDir']) && $GPOSinfo[$i]['wDir'] === 'RTL' && isset($GPOSinfo[$i]['XAdvanceR']) && $GPOSinfo[$i]['XAdvanceR']) { $XshiftAfter += $GPOSinfo[$i]['XAdvanceR'] * 1000 / $this->CurrentFont['unitsPerEm']; } - } // Character & Word spacing - if NOT OTL - else { + + } else { // Character & Word spacing - if NOT OTL $XshiftAfter += $charspacing; if ($c == 32) { $XshiftAfter += $wordspacing; @@ -5526,7 +5516,7 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) } } - if ($YPlacement != $lastYPlacement) { + if ($YPlacement !== $lastYPlacement) { $groupBreak = true; } @@ -5547,7 +5537,7 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) // $this->CurrentFont['subset'][$this->upperCase[$c]] = $this->upperCase[$c]; // add the CAP to subset $SmallCapsON = true; // For $sipset - if (!$lastSmallCapsON) { // Turn ON SmallCaps + if (!$lastSmallCapsON) { // Turn ON SmallCaps $groupBreak = true; $fontstretch = $this->smCapsStretch; $fontsize = $this->FontSizePt * $this->smCapsScale; @@ -5564,16 +5554,6 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) // Prepare Text and Select Font ID if ($sipset) { - // mPDF 6 DELETED ******** - // if ($c == 7 || $c == 8) { - // if ($original_fontid != $last_fontid) { - // $groupBreak = true; - // $fontid = $original_fontid; - // } - // if ($c == 7) { $tj .= $this->aliasNbPgHex; } - // else { $tj .= $this->aliasNbPgGpHex; } - // continue; - // } for ($j = 0; $j < 99; $j++) { $init = array_search($c, $this->CurrentFont['subsets'][$j]); if ($init !== false) { @@ -5582,8 +5562,11 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) $fontid = $this->CurrentFont['subsetfontids'][$j]; } $tx = sprintf("%02s", strtoupper(dechex($init))); + break; - } elseif (count($this->CurrentFont['subsets'][$j]) < 255) { + } + + if (count($this->CurrentFont['subsets'][$j]) < 255) { $n = count($this->CurrentFont['subsets'][$j]); $this->CurrentFont['subsets'][$j][$n] = $c; if ($this->CurrentFont['subsetfontids'][$j] != $last_fontid) { @@ -5591,44 +5574,53 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) $fontid = $this->CurrentFont['subsetfontids'][$j]; } $tx = sprintf("%02s", strtoupper(dechex($n))); + break; - } elseif (!isset($this->CurrentFont['subsets'][($j + 1)])) { + } + + if (!isset($this->CurrentFont['subsets'][($j + 1)])) { $this->CurrentFont['subsets'][($j + 1)] = [0 => 0]; $this->CurrentFont['subsetfontids'][($j + 1)] = count($this->fonts) + $this->extraFontSubsets + 1; $this->extraFontSubsets++; } } + } else { + $tx = UtfString::code2utf($c); + if ($this->usingCoreFont) { - $tx = utf8_decode($tx); + $tx = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $tx); } else { $tx = $this->writer->utf8ToUtf16BigEndian($tx, false); } + $tx = $this->writer->escape($tx); + } // If any settings require a new Text Group if ($groupBreak || $fontstretch != $last_fontstretch) { - if ($sipset) { - $tj .= '>] TJ '; - } else { - $tj .= ')] TJ '; - } + + $tj .= $sipset + ? '>] TJ ' + : ')] TJ '; + if ($fontid != $last_fontid || $fontsize != $last_fontsize) { $tj .= sprintf(' /F%d %.3F Tf ', $fontid, $fontsize); } + if ($fontstretch != $last_fontstretch) { $tj .= sprintf('%d Tz ', $fontstretch); } + if ($YPlacement != $lastYPlacement) { $tj .= sprintf('%.3F Ts ', $YPlacement); } - if ($sipset) { - $tj .= '[<'; - } else { - $tj .= '[('; - } + + $tj .= $sipset + ? '[<' + : '[('; } // Output the code for the txt character @@ -5649,32 +5641,43 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) // Get YPlacement from next Base character $nextbase = $i + 1; - while ($OTLdata['group'][$nextbase] != 'C') { + + while ($OTLdata['group'][$nextbase] !== 'C') { $nextbase++; } + if (isset($GPOSinfo[$nextbase]) && isset($GPOSinfo[$nextbase]['YPlacement']) && $GPOSinfo[$nextbase]['YPlacement']) { $YPlacement = $GPOSinfo[$nextbase]['YPlacement'] * $this->FontSizePt / $this->CurrentFont['unitsPerEm']; } // Prepare Text and Select Font ID if ($sipset) { + for ($j = 0; $j < 99; $j++) { + $init = array_search($c, $this->CurrentFont['subsets'][$j]); + if ($init !== false) { if ($this->CurrentFont['subsetfontids'][$j] != $last_fontid) { $fontid = $this->CurrentFont['subsetfontids'][$j]; } $tx = sprintf("%02s", strtoupper(dechex($init))); + break; - } elseif (count($this->CurrentFont['subsets'][$j]) < 255) { + } + + if (count($this->CurrentFont['subsets'][$j]) < 255) { $n = count($this->CurrentFont['subsets'][$j]); $this->CurrentFont['subsets'][$j][$n] = $c; if ($this->CurrentFont['subsetfontids'][$j] != $last_fontid) { $fontid = $this->CurrentFont['subsetfontids'][$j]; } $tx = sprintf("%02s", strtoupper(dechex($n))); + break; - } elseif (!isset($this->CurrentFont['subsets'][($j + 1)])) { + } + + if (!isset($this->CurrentFont['subsets'][($j + 1)])) { $this->CurrentFont['subsets'][($j + 1)] = [0 => 0]; $this->CurrentFont['subsetfontids'][($j + 1)] = count($this->fonts) + $this->extraFontSubsets + 1; $this->extraFontSubsets++; @@ -5687,40 +5690,45 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) } if ($kashida > $tatw) { + // Insert multiple tatweel characters, repositioning the last one to give correct total length + $fontstretch = 100; - $nt = intval($kashida / $tatw); + $nt = (int) ($kashida / $tatw); $nudgeback = (($nt + 1) * $tatw) - $kashida; $optx = str_repeat($tx, $nt); + if ($sipset) { $optx .= sprintf('>%d<', ($nudgeback)); } else { $optx .= sprintf(')%d(', ($nudgeback)); } + $optx .= $tx; // #last + } else { // Insert single tatweel character and use fontstretch to get correct length $fontstretch = ($kashida / $tatw) * 100; $optx = $tx; } - if ($sipset) { - $tj .= '>] TJ '; - } else { - $tj .= ')] TJ '; - } + $tj .= $sipset + ? '>] TJ ' + : ')] TJ '; + if ($fontid != $last_fontid || $fontsize != $last_fontsize) { $tj .= sprintf(' /F%d %.3F Tf ', $fontid, $fontsize); } + if ($fontstretch != $last_fontstretch) { $tj .= sprintf('%d Tz ', $fontstretch); } + $tj .= sprintf('%.3F Ts ', $YPlacement); - if ($sipset) { - $tj .= '[<'; - } else { - $tj .= '[('; - } + + $tj .= $sipset + ? '[<' + : '[('; // Output the code for the txt character(s) $tj .= $optx; @@ -5732,101 +5740,116 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) $lastYPlacement = $YPlacement; } + $tj .= $sipset + ? '>' + : ')'; - // Finish up - if ($sipset) { - $tj .= '>'; - if ($XshiftAfter) { - $tj .= sprintf('%d', (-$XshiftAfter)); - } - if ($last_fontid != $original_fontid) { - $tj .= '] TJ '; - $tj .= sprintf(' /F%d %.3F Tf ', $original_fontid, $fontsize); - $tj .= '['; - } - $tj = preg_replace('/([^\\\])<>/', '\\1 ', $tj); - } else { - $tj .= ')'; - if ($XshiftAfter) { - $tj .= sprintf('%d', (-$XshiftAfter)); - } - if ($last_fontid != $original_fontid) { - $tj .= '] TJ '; - $tj .= sprintf(' /F%d %.3F Tf ', $original_fontid, $fontsize); - $tj .= '['; - } - $tj = preg_replace('/([^\\\])\(\)/', '\\1 ', $tj); + if ($XshiftAfter) { + $tj .= sprintf('%d', (-$XshiftAfter)); } - $s = sprintf(' BT ' . $aix . ' 0 Tc 0 Tw [%s] TJ ET ', $x, $y, $tj); + if ($last_fontid != $original_fontid) { + $tj .= '] TJ '; + $tj .= sprintf(' /F%d %.3F Tf ', $original_fontid, $fontsize); + $tj .= '['; + } - // echo $s."\n\n"; // exit; + $tj = $sipset + ? preg_replace('/([^\\\])<>/', '\\1 ', $tj) + : preg_replace('/([^\\\])\(\)/', '\\1 ', $tj); - return $s; + return sprintf(' BT ' . $aix . ' 0 Tc 0 Tw [%s] TJ ET ', $x, $y, $tj); } function _kern($txt, $mode, $aix, $x, $y) { - if ($mode == 'MBTw') { // Multibyte requiring word spacing + if ($mode === 'MBTw') { // Multibyte requiring word spacing + $space = ' '; + // Convert string to UTF-16BE without BOM $space = $this->writer->utf8ToUtf16BigEndian($space, false); $space = $this->writer->escape($space); + $s = sprintf(' BT ' . $aix, $x * Mpdf::SCALE, ($this->h - $y) * Mpdf::SCALE); $t = explode(' ', $txt); - for ($i = 0; $i < count($t); $i++) { - $tx = $t[$i]; + + foreach ($t as $i => $iValue) { + $tx = $iValue; $tj = '('; $unicode = $this->UTF8StringToArray($tx); - for ($ti = 0; $ti < count($unicode); $ti++) { - if ($ti > 0 && isset($this->CurrentFont['kerninfo'][$unicode[($ti - 1)]][$unicode[$ti]])) { - $kern = -$this->CurrentFont['kerninfo'][$unicode[($ti - 1)]][$unicode[$ti]]; + + foreach ($unicode as $ti => $tiValue) { + + if ($ti > 0 && isset($this->CurrentFont['kerninfo'][$unicode[($ti - 1)]][$tiValue])) { + $kern = -$this->CurrentFont['kerninfo'][$unicode[($ti - 1)]][$tiValue]; $tj .= sprintf(')%d(', $kern); } - $tc = UtfString::code2utf($unicode[$ti]); + + $tc = UtfString::code2utf($tiValue); $tc = $this->writer->utf8ToUtf16BigEndian($tc, false); $tj .= $this->writer->escape($tc); } + $tj .= ')'; $s .= sprintf(' %.3F Tc [%s] TJ', $this->charspacing, $tj); - if (($i + 1) < count($t)) { $s .= sprintf(' %.3F Tc (%s) Tj', $this->ws + $this->charspacing, $space); } } + $s .= ' ET '; - } elseif (!$this->usingCoreFont) { + + return $s; + + } + + if (!$this->usingCoreFont) { + $s = ''; $tj = '('; + $unicode = $this->UTF8StringToArray($txt); - for ($i = 0; $i < count($unicode); $i++) { - if ($i > 0 && isset($this->CurrentFont['kerninfo'][$unicode[($i - 1)]][$unicode[$i]])) { - $kern = -$this->CurrentFont['kerninfo'][$unicode[($i - 1)]][$unicode[$i]]; + + foreach ($unicode as $i => $iValue) { + + if ($i > 0 && isset($this->CurrentFont['kerninfo'][$unicode[($i - 1)]][$iValue])) { + $kern = -$this->CurrentFont['kerninfo'][$unicode[($i - 1)]][$iValue]; $tj .= sprintf(')%d(', $kern); } - $tx = UtfString::code2utf($unicode[$i]); + + $tx = UtfString::code2utf($iValue); $tx = $this->writer->utf8ToUtf16BigEndian($tx, false); $tj .= $this->writer->escape($tx); + } + $tj .= ')'; $s .= sprintf(' BT ' . $aix . ' [%s] TJ ET ', $x * Mpdf::SCALE, ($this->h - $y) * Mpdf::SCALE, $tj); - } else { // CORE Font - $s = ''; - $tj = '('; - $l = strlen($txt); - for ($i = 0; $i < $l; $i++) { - if ($i > 0 && isset($this->CurrentFont['kerninfo'][$txt[($i - 1)]][$txt[$i]])) { - $kern = -$this->CurrentFont['kerninfo'][$txt[($i - 1)]][$txt[$i]]; - $tj .= sprintf(')%d(', $kern); - } - $tj .= $this->writer->escape($txt[$i]); + + return $s; + + } + + $s = ''; + $tj = '('; + $l = strlen($txt); + + for ($i = 0; $i < $l; $i++) { + + if ($i > 0 && isset($this->CurrentFont['kerninfo'][$txt[($i - 1)]][$txt[$i]])) { + $kern = -$this->CurrentFont['kerninfo'][$txt[($i - 1)]][$txt[$i]]; + $tj .= sprintf(')%d(', $kern); } - $tj .= ')'; - $s .= sprintf(' BT ' . $aix . ' [%s] TJ ET ', $x * Mpdf::SCALE, ($this->h - $y) * Mpdf::SCALE, $tj); + + $tj .= $this->writer->escape($txt[$i]); } + $tj .= ')'; + $s .= sprintf(' BT ' . $aix . ' [%s] TJ ET ', $x * Mpdf::SCALE, ($this->h - $y) * Mpdf::SCALE, $tj); + return $s; } @@ -7270,7 +7293,7 @@ function printobjectbuffer($is_table = false, $blockdir = false) $translate_x = $this->sizeConverter->convert($vv[0], $maxsize_x, false, false); $tr2 .= $this->transformTranslate($translate_x, 0, true) . ' '; } elseif ($c == 'translatey' && count($vv)) { - $translate_y = $this->sizeConverter->convert($vv[1], $maxsize_y, false, false); + $translate_y = $this->sizeConverter->convert($vv[0], $maxsize_y, false, false); $tr2 .= $this->transformTranslate(0, $translate_y, true) . ' '; } elseif ($c == 'scale' && count($vv)) { $scale_x = $vv[0] * 100; @@ -7284,7 +7307,7 @@ function printobjectbuffer($is_table = false, $blockdir = false) $scale_x = $vv[0] * 100; $tr2 .= $this->transformScale($scale_x, 0, $cx, $cy, true) . ' '; } elseif ($c == 'scaley' && count($vv)) { - $scale_y = $vv[1] * 100; + $scale_y = $vv[0] * 100; $tr2 .= $this->transformScale(0, $scale_y, $cx, $cy, true) . ' '; } elseif ($c == 'skew' && count($vv)) { $angle_x = $this->ConvertAngle($vv[0], false); @@ -7831,7 +7854,7 @@ function WriteFlowingBlock($s, $sOTLdata) /* -- END CSS-IMAGE-FLOAT -- */ } // *TABLES* // OBJECTS - IMAGES & FORM Elements (NB has already skipped line/page if required - in printbuffer) - if (substr($s, 0, 3) == "\xbb\xa4\xac") { // identifier has been identified! + if (substr($s, 0, 3) == Mpdf::OBJECT_IDENTIFIER) { // identifier has been identified! $objattr = $this->_getObjAttr($s); $h_corr = 0; if ($is_table) { // *TABLES* @@ -8212,8 +8235,13 @@ function WriteFlowingBlock($s, $sOTLdata) $contentB[count($content) - 1] = preg_replace('/R/', '', $contentB[count($content) - 1]); // ??? } - if ($type == 'hyphen') { - $currContent .= '-'; + if ($type === 'hyphen') { + $hyphen = in_array(mb_substr($currContent, -1), ['-', '–', '—'], true); + if (!$hyphen) { + $currContent .= '-'; + } else { + $savedPreContent[count($savedPreContent) - 1] = '-' . $savedPreContent[count($savedPreContent) - 1]; + } if (!empty($cOTLdata[(count($cOTLdata) - 1)])) { $cOTLdata[(count($cOTLdata) - 1)]['char_data'][] = ['bidi_class' => 9, 'uni' => 45]; $cOTLdata[(count($cOTLdata) - 1)]['group'] .= 'C'; @@ -8857,14 +8885,14 @@ function Image($file, $x, $y, $w = 0, $h = 0, $type = '', $link = '', $paint = t // Automatic width and height calculation if needed if ($w == 0 and $h == 0) { /* -- IMAGES-WMF -- */ - if ($info['type'] == 'wmf') { + if ($info['type'] === 'wmf') { // WMF units are twips (1/20pt) // divide by 20 to get points // divide by k to get user units $w = abs($info['w']) / (20 * Mpdf::SCALE); $h = abs($info['h']) / (20 * Mpdf::SCALE); } else { /* -- END IMAGES-WMF -- */ - if ($info['type'] == 'svg') { + if ($info['type'] === 'svg') { // returned SVG units are pts // divide by k to get user units (mm) $w = abs($info['w']) / Mpdf::SCALE; @@ -9073,12 +9101,14 @@ function Image($file, $x, $y, $w = 0, $h = 0, $type = '', $link = '', $paint = t function _getObjAttr($t) { - $c = explode("\xbb\xa4\xac", $t, 2); - $c = explode(",", $c[1], 2); + $c = explode(Mpdf::OBJECT_IDENTIFIER, $t, 2); + $c = explode(',', $c[1], 2); + foreach ($c as $v) { - $v = explode("=", $v, 2); - $sp[$v[0]] = $v[1]; + $v = explode('=', $v, 2); + $sp[$v[0]] = trim($v[1], Mpdf::OBJECT_IDENTIFIER); } + return (unserialize($sp['objattr'])); } @@ -9625,6 +9655,32 @@ function Output($name = '', $dest = '') $this->cache->clearOld(); } + public function OutputBinaryData() + { + return $this->Output(null, Destination::STRING_RETURN); + } + + public function OutputHttpInline() + { + return $this->Output(null, Destination::INLINE); + } + + /** + * @param string $fileName + */ + public function OutputHttpDownload($fileName) + { + return $this->Output($fileName, Destination::DOWNLOAD); + } + + /** + * @param string $fileName + */ + public function OutputFile($fileName) + { + return $this->Output($fileName, Destination::FILE); + } + // ***************************************************************************** // * // Protected methods * @@ -9651,6 +9707,7 @@ function _dochecks() } if (!function_exists('mb_regex_encoding')) { + $mamp = ''; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $mamp = ' If using MAMP, there is a bug in its PHP build causing this.'; } @@ -9696,6 +9753,13 @@ function _puthtmlheaders() $this->margin_footer = $this->saveHTMLHeader[$n][$OE]['mf']; $this->w = $this->saveHTMLHeader[$n][$OE]['pw']; $this->h = $this->saveHTMLHeader[$n][$OE]['ph']; + if ($this->w > $this->h) { + $this->hPt = $this->fwPt; + $this->wPt = $this->fhPt; + } else { + $this->hPt = $this->fhPt; + $this->wPt = $this->fwPt; + } $rotate = (isset($this->saveHTMLHeader[$n][$OE]['rotate']) ? $this->saveHTMLHeader[$n][$OE]['rotate'] : null); $this->Reset(); $this->pageoutput[$n] = []; @@ -9770,6 +9834,13 @@ function _puthtmlheaders() $this->margin_footer = $this->saveHTMLFooter[$n][$OE]['mf']; $this->w = $this->saveHTMLFooter[$n][$OE]['pw']; $this->h = $this->saveHTMLFooter[$n][$OE]['ph']; + if ($this->w > $this->h) { + $this->hPt = $this->fwPt; + $this->wPt = $this->fhPt; + } else { + $this->hPt = $this->fhPt; + $this->wPt = $this->fwPt; + } $rotate = (isset($this->saveHTMLFooter[$n][$OE]['rotate']) ? $this->saveHTMLFooter[$n][$OE]['rotate'] : null); $this->Reset(); $this->pageoutput[$n] = []; @@ -9972,9 +10043,10 @@ function _enddoc() for ($i = 0; $i < 4; $i++) { $m[$i] = array_merge($m1[$i], $m2[$i], $m3[$i]); } - if (count($m[0])) { + $mFirstLength = count($m[0]); + if ($mFirstLength) { $sortarr = []; - for ($i = 0; $i < count($m[0]); $i++) { + for ($i = 0; $i < $mFirstLength; $i++) { $key = $m[1][$i] * 2; if ($m[3][$i] == 'EMCZ') { $key +=2; // background first then gradient then normal @@ -10084,6 +10156,7 @@ function _beginpage( $this->page++; $this->pages[$this->page] = ''; } + $this->state = 2; $resetHTMLHeadersrequired = false; @@ -10093,15 +10166,15 @@ function _beginpage( /* -- CSS-PAGE -- */ // Paged media (page-box) - if ($pagesel || (isset($this->page_box['using']) && $this->page_box['using'])) { + if ($pagesel || $this->page_box['using']) { - if ($pagesel || $this->page == 1) { + if ($pagesel || $this->page === 1) { $first = true; } else { $first = false; } - if ($this->mirrorMargins && ($this->page % 2 == 0)) { + if ($this->mirrorMargins && ($this->page % 2 === 0)) { $oddEven = 'E'; } else { $oddEven = 'O'; @@ -10117,7 +10190,7 @@ function _beginpage( list($orientation, $mgl, $mgr, $mgt, $mgb, $mgh, $mgf, $hname, $fname, $bg, $resetpagenum, $pagenumstyle, $suppress, $marks, $newformat) = $this->SetPagedMediaCSS($psel, $first, $oddEven); - if ($this->mirrorMargins && ($this->page % 2 == 0)) { + if ($this->mirrorMargins && ($this->page % 2 === 0)) { if ($hname) { $ehvalue = 1; @@ -10200,15 +10273,15 @@ function _beginpage( } } - if ($orientation != $this->CurOrientation || $newformat) { + if ($orientation !== $this->CurOrientation || $newformat) { // Change orientation - if ($orientation == 'P') { + if ($orientation === 'P') { $this->wPt = $this->fwPt; $this->hPt = $this->fhPt; $this->w = $this->fw; $this->h = $this->fh; - if (($this->forcePortraitHeaders || $this->forcePortraitMargins) && $this->DefOrientation == 'P') { + if (($this->forcePortraitHeaders || $this->forcePortraitMargins) && $this->DefOrientation === 'P') { $this->tMargin = $this->orig_tMargin; $this->bMargin = $this->orig_bMargin; $this->DeflMargin = $this->orig_lMargin; @@ -10224,7 +10297,7 @@ function _beginpage( $this->w = $this->fh; $this->h = $this->fw; - if (($this->forcePortraitHeaders || $this->forcePortraitMargins) && $this->DefOrientation == 'P') { + if (($this->forcePortraitHeaders || $this->forcePortraitMargins) && $this->DefOrientation === 'P') { $this->tMargin = $this->orig_lMargin; $this->bMargin = $this->orig_rMargin; $this->DeflMargin = $this->orig_bMargin; @@ -10245,10 +10318,10 @@ function _beginpage( $this->pageDim[$this->page]['w'] = $this->w; $this->pageDim[$this->page]['h'] = $this->h; - $this->pageDim[$this->page]['outer_width_LR'] = isset($this->page_box['outer_width_LR']) ? $this->page_box['outer_width_LR'] : 0; - $this->pageDim[$this->page]['outer_width_TB'] = isset($this->page_box['outer_width_TB']) ? $this->page_box['outer_width_TB'] : 0; + $this->pageDim[$this->page]['outer_width_LR'] = $this->page_box['outer_width_LR'] ?: 0; + $this->pageDim[$this->page]['outer_width_TB'] = $this->page_box['outer_width_TB'] ?: 0; - if (!isset($this->page_box['outer_width_LR']) && !isset($this->page_box['outer_width_TB'])) { + if (!$this->page_box['outer_width_LR'] && !$this->page_box['outer_width_TB']) { $this->pageDim[$this->page]['bleedMargin'] = 0; } elseif ($this->bleedMargin <= $this->page_box['outer_width_LR'] && $this->bleedMargin <= $this->page_box['outer_width_TB']) { $this->pageDim[$this->page]['bleedMargin'] = $this->bleedMargin; @@ -10528,7 +10601,8 @@ function watermark($texte, $angle = 45, $fontsize = 96, $alpha = 0.2) $this->SetAlpha($alpha); - $this->SetTColor($this->colorConverter->convert(0, $this->PDFAXwarnings)); + $color = $this->watermarkTextObject ? $this->watermarkTextObject->getColor() : 0; + $this->SetTColor($this->colorConverter->convert($color, $this->PDFAXwarnings)); $szfont = $fontsize; $loop = 0; @@ -10558,6 +10632,7 @@ function watermark($texte, $angle = 45, $fontsize = 96, $alpha = 0.2) $this->Rotate($angle, $wx, $wy); $this->Text($wx, $wy, $texte, $OTLdata, $textvar); $this->Rotate(0); + $this->SetTColor($this->colorConverter->convert(0, $this->PDFAXwarnings)); $this->SetAlpha(1); @@ -11411,16 +11486,21 @@ function SetBasePath($str = '') } else { $host = ''; } + if (!$str) { + if (isset($_SERVER['SCRIPT_NAME'])) { $currentPath = dirname($_SERVER['SCRIPT_NAME']); } else { $currentPath = dirname($_SERVER['PHP_SELF']); } + $currentPath = str_replace("\\", "/", $currentPath); + if ($currentPath == '/') { $currentPath = ''; } + if ($host) { // mPDF 6 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] && $_SERVER['HTTPS'] !== 'off') { $currpath = 'https://' . $host . $currentPath . '/'; @@ -11430,27 +11510,33 @@ function SetBasePath($str = '') } else { $currpath = ''; } + $this->basepath = $currpath; $this->basepathIsLocal = true; + return; } + $str = preg_replace('/\?.*/', '', $str); + if (!preg_match('/(http|https|ftp):\/\/.*\//i', $str)) { $str .= '/'; } + $str .= 'xxx'; // in case $str ends in / e.g. http://www.bbc.co.uk/ + $this->basepath = dirname($str) . "/"; // returns e.g. e.g. http://www.google.com/dir1/dir2/dir3/ $this->basepath = str_replace("\\", "/", $this->basepath); // If on Windows + $tr = parse_url($this->basepath); - if (isset($tr['host']) && ($tr['host'] == $host)) { - $this->basepathIsLocal = true; - } else { - $this->basepathIsLocal = false; - } + + $this->basepathIsLocal = (isset($tr['host']) && ($tr['host'] == $host)); } public function GetFullPath(&$path, $basepath = '') { + // @todo make return, remove reference + // When parsing CSS need to pass temporary basepath - so links are relative to current stylesheet if (!$basepath) { $basepath = $this->basepath; @@ -11460,7 +11546,7 @@ public function GetFullPath(&$path, $basepath = '') $path = str_replace("\\", '/', $path); // If on Windows // mPDF 5.7.2 - if (substr($path, 0, 2) === '//') { + if (strpos($path, '//') === 0) { $scheme = parse_url($basepath, PHP_URL_SCHEME); $scheme = $scheme ?: 'http'; $path = $scheme . ':' . $path; @@ -11468,7 +11554,7 @@ public function GetFullPath(&$path, $basepath = '') $path = preg_replace('|^./|', '', $path); // Inadvertently corrects "./path/etc" and "//www.domain.com/etc" - if (substr($path, 0, 1) == '#') { + if (strpos($path, '#') === 0) { return; } @@ -11479,11 +11565,11 @@ public function GetFullPath(&$path, $basepath = '') return; } - if (substr($path, 0, 3) == "../") { // It is a relative link + if (strpos($path, '../') === 0) { // It is a relative link - $backtrackamount = substr_count($path, "../"); - $maxbacktrack = substr_count($basepath, "/") - 3; - $filepath = str_replace("../", '', $path); + $backtrackamount = substr_count($path, '../'); + $maxbacktrack = substr_count($basepath, '/') - 3; + $filepath = str_replace('../', '', $path); $path = $basepath; // If it is an invalid relative link, then make it go to directory root @@ -11496,11 +11582,15 @@ public function GetFullPath(&$path, $basepath = '') $path = substr($path, 0, strrpos($path, "/")); } - $path = $path . "/" . $filepath; // Make it an absolute path + $path .= '/' . $filepath; // Make it an absolute path - } elseif ((strpos($path, ":/") === false || strpos($path, ":/") > 10) && !@is_file($path)) { // It is a local link. Ignore potential file errors + return; + + } - if (substr($path, 0, 1) == "/") { + if ((strpos($path, ":/") === false || strpos($path, ":/") > 10) && !@is_file($path)) { // It is a local link. Ignore potential file errors + + if (strpos($path, '/') === 0) { $tr = parse_url($basepath); @@ -11515,10 +11605,13 @@ public function GetFullPath(&$path, $basepath = '') $path = $root . $path; - } else { - $path = $basepath . $path; + return; + } + + $path = $basepath . $path; } + // Do nothing if it is an Absolute Link } @@ -13004,17 +13097,41 @@ function SetFooter($Farray = [], $side = '') function SetWatermarkText($txt = '', $alpha = -1) { + if ($txt instanceof \Mpdf\WatermarkText) { + $this->watermarkTextObject = $txt; + $this->watermarkText = $txt->getText(); + $this->watermarkTextAlpha = $txt->getAlpha(); + $this->watermarkAngle = $txt->getAngle(); + $this->watermark_font = $txt->getFont() !== null ? $txt->getFont() : $this->watermark_font; + $this->watermark_size = $txt->getSize(); + + return; + } + if ($alpha >= 0) { $this->watermarkTextAlpha = $alpha; } + $this->watermarkText = $txt; } function SetWatermarkImage($src, $alpha = -1, $size = 'D', $pos = 'F') { + if ($src instanceof \Mpdf\WatermarkImage) { + $this->watermarkImage = $src->getPath(); + $this->watermark_size = $src->getSize(); + $this->watermark_pos = $src->getPosition(); + $this->watermarkImageAlpha = $src->getAlpha(); + $this->watermarkImgBehind = $src->isBehindContent(); + $this->watermarkImgAlphaBlend = $src->getAlphaBlend(); + + return; + } + if ($alpha >= 0) { $this->watermarkImageAlpha = $alpha; } + $this->watermarkImage = $src; $this->watermark_size = $size; $this->watermark_pos = $pos; @@ -13122,7 +13239,7 @@ function Footer() /* -- WATERMARK -- */ if (($this->watermarkText) && ($this->showWatermarkText)) { - $this->watermark($this->watermarkText, $this->watermarkAngle, 120, $this->watermarkTextAlpha); // Watermark text + $this->watermark($this->watermarkText, $this->watermarkAngle, is_int($this->watermark_size) ? $this->watermark_size : 120, $this->watermarkTextAlpha); // Watermark text } if (($this->watermarkImage) && ($this->showWatermarkImage)) { $this->watermarkImg($this->watermarkImage, $this->watermarkImageAlpha); // Watermark image @@ -13532,7 +13649,7 @@ function WriteHTML($html, $mode = HTMLParserMode::DEFAULT_MODE, $init = true, $c $objattr['text'] = $e; $objattr['OTLdata'] = $this->OTLdata; $this->OTLdata = []; - $te = "\xbb\xa4\xactype=textarea,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $te = Mpdf::OBJECT_IDENTIFIER . "type=textarea,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->tdbegin) { $this->_saveCellTextBuffer($te, $this->HREF); } else { @@ -15455,7 +15572,7 @@ function _setListMarker($listitemtype, $listitemimage, $listitemposition) $objattr['listmarkerposition'] = $listitemposition; - $e = "\xbb\xa4\xactype=image,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=image,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->_saveTextBuffer($e); if ($listitemposition == 'inside') { @@ -15486,7 +15603,7 @@ function _setListMarker($listitemtype, $listitemimage, $listitemposition) $objattr['fontsizept'] = $this->FontSizePt; $objattr['fontstyle'] = $this->FontStyle; - $e = "\xbb\xa4\xactype=listmarker,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=listmarker,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->listitem = $this->_saveTextBuffer($e, '', '', true); // true returns array } elseif (preg_match('/U\+([a-fA-F0-9]+)/i', $listitemtype, $m)) { // SYMBOL 2 (needs new font) @@ -15524,7 +15641,7 @@ function _setListMarker($listitemtype, $listitemimage, $listitemposition) $objattr['fontsize'] = $this->FontSize; $objattr['fontsizept'] = $this->FontSizePt; $objattr['fontstyle'] = $this->FontStyle; - $e = "\xbb\xa4\xactype=listmarker,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=listmarker,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->listitem = $this->_saveTextBuffer($e, '', '', true); // true returns array } @@ -15563,7 +15680,7 @@ function _setListMarker($listitemtype, $listitemimage, $listitemposition) $objattr['fontsize'] = $this->FontSize; $objattr['fontsizept'] = $this->FontSizePt; $objattr['fontstyle'] = $this->FontStyle; - $e = "\xbb\xa4\xactype=listmarker,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=listmarker,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->listitem = $this->_saveTextBuffer($e, '', '', true); // true returns array } @@ -15956,7 +16073,7 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft // First make sure each element/chunk has the OTLdata for Bidi set. for ($i = 0; $i < $array_size; $i++) { if (empty($arrayaux[$i][18])) { - if (substr($arrayaux[$i][0], 0, 3) == "\xbb\xa4\xac") { // object identifier has been identified! + if (substr($arrayaux[$i][0], 0, 3) == Mpdf::OBJECT_IDENTIFIER) { // object identifier has been identified! $unicode = [0xFFFC]; // Object replacement character } else { $unicode = $this->UTF8StringToArray($arrayaux[$i][0], false); @@ -15979,10 +16096,9 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft $array_size = count($arrayaux); } - // Remove empty items // mPDF 6 for ($i = $array_size - 1; $i > 0; $i--) { - if (empty($arrayaux[$i][0]) && (isset($arrayaux[$i][16]) && $arrayaux[$i][16] !== '0') && empty($arrayaux[$i][7])) { + if ('' === $arrayaux[$i][0] && (isset($arrayaux[$i][16]) && $arrayaux[$i][16] !== '0') && empty($arrayaux[$i][7])) { unset($arrayaux[$i]); } } @@ -16029,7 +16145,8 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft } // FIXED TO ALLOW IT TO SHOW '0' - if (empty($vetor[0]) && !($vetor[0] === '0') && empty($vetor[7])) { // Ignore empty text and not carrying an internal link + if (empty($vetor[0]) && !($vetor[0] === '0') && empty($vetor[7])) { + // Ignore empty text and not carrying an internal link // Check if it is the last element. If so then finish printing the block if ($i == ($array_size - 1)) { $this->finishFlowingBlock(true); @@ -16143,7 +16260,7 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft // SPECIAL CONTENT - IMAGES & FORM OBJECTS // Print-out special content - if (substr($vetor[0], 0, 3) == "\xbb\xa4\xac") { // identifier has been identified! + if (substr($vetor[0], 0, 3) == Mpdf::OBJECT_IDENTIFIER) { // identifier has been identified! $objattr = $this->_getObjAttr($vetor[0]); /* -- TABLES -- */ @@ -19070,7 +19187,7 @@ function TableCheckMinWidth($maxwidth, $forcewrap = 0, $textbuffer = [], $checkl } // IMAGES & FORM ELEMENTS - if (substr($line, 0, 3) == "\xbb\xa4\xac") { // inline object - FORM element or IMAGE! + if (substr($line, 0, 3) == Mpdf::OBJECT_IDENTIFIER) { // inline object - FORM element or IMAGE! $objattr = $this->_getObjAttr($line); if ($objattr['type'] != 'hr' && isset($objattr['width']) && ($objattr['width'] / $this->shrin_k) > ($maxwidth + 0.0001)) { if (($objattr['width'] / $this->shrin_k) > $biggestword) { @@ -20730,7 +20847,7 @@ function _tableGetMaxRowHeight($table, $row) for ($i = $row + 1; $i < $table['nr']; $i++) { $cellsset = 0; for ($j = 0; $j < $table['nc']; $j++) { - if ($table['cells'][$i][$j]) { + if (!empty($table['cells'][$i][$j])) { if (isset($table['cells'][$i][$j]['colspan'])) { $cellsset += $table['cells'][$i][$j]['colspan']; } else { @@ -21845,10 +21962,10 @@ function _reverseTableDir(&$table) if (isset($cell['textbuffer'])) { for ($n = 0; $n < count($cell['textbuffer']); $n++) { $t = $cell['textbuffer'][$n][0]; - if (substr($t, 0, 19) == "\xbb\xa4\xactype=nestedtable") { + if (substr($t, 0, 19) == Mpdf::OBJECT_IDENTIFIER . "type=nestedtable") { $objattr = $this->_getObjAttr($t); $objattr['col'] = $col; - $cell['textbuffer'][$n][0] = "\xbb\xa4\xactype=nestedtable,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $cell['textbuffer'][$n][0] = Mpdf::OBJECT_IDENTIFIER . "type=nestedtable,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->table[($this->tableLevel + 1)][$objattr['nestedcontent']]['nestedpos'][1] = $col; } } @@ -22129,7 +22246,18 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $extra = $table['max_cell_border_width']['B'] / 2; } - if ($j == $startcol && ((($y + $maxrowheight + $extra ) > ($pagetrigger + 0.001)) || (($this->keepColumns || !$this->ColActive) && !empty($tablefooter) && ($y + $maxrowheight + $tablefooterrowheight + $extra) > $pagetrigger) && ($this->tableLevel == 1 && $i < ($numrows - $table['headernrows']))) && ($y0 > 0 || $x0 > 0) && !$this->InFooter && $this->autoPageBreak) { + // lookahead for pagebreak + $pagebreaklookahead = 1; + while (isset($table['pagebreak-before']) && isset($table['pagebreak-before'][$i + $pagebreaklookahead]) && $table['pagebreak-before'][$i + $pagebreaklookahead] == 'avoid') { + // pagebreak-after is mapped to pagebreak-before on i+1 in Tags/Tr.php + $pagebreaklookahead++; + } + // corner case: if the pagelookahead is bigger than the pagesize, we break anyway, so fill up the page + if ($pagebreaklookahead * $maxrowheight + $extra > $pagetrigger + 0.001) { + $pagebreaklookahead = 1; + } + // if we exceed page boundaries: restart table on next page before printing the line + if ($j == $startcol && ((($y + $pagebreaklookahead * $maxrowheight + $extra ) > ($pagetrigger + 0.001)) || (($this->keepColumns || !$this->ColActive) && !empty($tablefooter) && ($y + $maxrowheight + $tablefooterrowheight + $extra) > $pagetrigger) && ($this->tableLevel == 1 && $i < ($numrows - $table['headernrows']))) && ($y0 > 0 || $x0 > 0) && !$this->InFooter && $this->autoPageBreak) { if (!$skippage) { $finalSpread = true; $firstSpread = true; @@ -23773,26 +23901,27 @@ function DeletePages($start_page, $end_page = -1) if ($end_page < 1) { $end_page = $start_page; } - $n_tod = $end_page - $start_page + 1; - $last_page = count($this->pages); - $n_atend = $last_page - $end_page + 1; + + $deletedPagesCount = $end_page - $start_page + 1; + $lastPageNumber = count($this->pages); + $remainingPagesFromEndPageCount = $lastPageNumber - $end_page; // move pages - for ($i = 0; $i < $n_atend; $i++) { + for ($i = 0; $i < $remainingPagesFromEndPageCount; $i++) { $this->pages[$start_page + $i] = $this->pages[$end_page + 1 + $i]; } + // delete pages - for ($i = 0; $i < $n_tod; $i++) { - unset($this->pages[$last_page - $i]); + for ($i = 0; $i < $deletedPagesCount; $i++) { + unset($this->pages[$lastPageNumber - $i]); } - /* -- BOOKMARKS -- */ // Update Bookmarks foreach ($this->BMoutlines as $i => $o) { if ($o['p'] >= $end_page) { - $this->BMoutlines[$i]['p'] -= $n_tod; - } elseif ($p < $start_page) { + $this->BMoutlines[$i]['p'] -= $deletedPagesCount; + } elseif ($o['p'] < $start_page) { unset($this->BMoutlines[$i]); } } @@ -23806,14 +23935,14 @@ function DeletePages($start_page, $end_page = -1) if (strpos($pl[4], '@') === 0) { $p = substr($pl[4], 1); if ($p > $end_page) { - $this->PageLinks[$i][$key][4] = '@' . ($p - $n_tod); + $this->PageLinks[$i][$key][4] = '@' . ($p - $deletedPagesCount); } elseif ($p < $start_page) { unset($this->PageLinks[$i][$key]); } } } if ($i > $end_page) { - $newarr[($i - $n_tod)] = $this->PageLinks[$i]; + $newarr[($i - $deletedPagesCount)] = $this->PageLinks[$i]; } elseif ($p < $start_page) { $newarr[$i] = $this->PageLinks[$i]; } @@ -23840,7 +23969,7 @@ function DeletePages($start_page, $end_page = -1) $newarr = []; foreach ($this->pageDim as $p => $v) { if ($p > $end_page) { - $newarr[($p - $n_tod)] = $this->pageDim[$p]; + $newarr[($p - $deletedPagesCount)] = $this->pageDim[$p]; } elseif ($p < $start_page) { $newarr[$p] = $this->pageDim[$p]; } @@ -23853,7 +23982,7 @@ function DeletePages($start_page, $end_page = -1) if (count($this->saveHTMLHeader)) { foreach ($this->saveHTMLHeader as $p => $v) { if ($p > $end_page) { - $newarr[($p - $n_tod)] = $this->saveHTMLHeader[$p]; + $newarr[($p - $deletedPagesCount)] = $this->saveHTMLHeader[$p]; } // mPDF 5.7.3 elseif ($p < $start_page) { $newarr[$p] = $this->saveHTMLHeader[$p]; @@ -23866,7 +23995,7 @@ function DeletePages($start_page, $end_page = -1) $newarr = []; foreach ($this->saveHTMLFooter as $p => $v) { if ($p > $end_page) { - $newarr[($p - $n_tod)] = $this->saveHTMLFooter[$p]; + $newarr[($p - $deletedPagesCount)] = $this->saveHTMLFooter[$p]; } elseif ($p < $start_page) { $newarr[$p] = $this->saveHTMLFooter[$p]; } @@ -23878,7 +24007,7 @@ function DeletePages($start_page, $end_page = -1) // Update Internal Links foreach ($this->internallink as $key => $o) { if ($o['PAGE'] > $end_page) { - $this->internallink[$key]['PAGE'] -= $n_tod; + $this->internallink[$key]['PAGE'] -= $deletedPagesCount; } elseif ($o['PAGE'] < $start_page) { unset($this->internallink[$key]); } @@ -23887,7 +24016,7 @@ function DeletePages($start_page, $end_page = -1) // Update Links foreach ($this->links as $key => $o) { if ($o[0] > $end_page) { - $this->links[$key][0] -= $n_tod; + $this->links[$key][0] -= $deletedPagesCount; } elseif ($o[0] < $start_page) { unset($this->links[$key]); } @@ -23896,7 +24025,7 @@ function DeletePages($start_page, $end_page = -1) // Update Form fields foreach ($this->form->forms as $key => $f) { if ($f['page'] > $end_page) { - $this->form->forms[$key]['page'] -= $n_tod; + $this->form->forms[$key]['page'] -= $deletedPagesCount; } elseif ($f['page'] < $start_page) { unset($this->form->forms[$key]); } @@ -23909,7 +24038,7 @@ function DeletePages($start_page, $end_page = -1) foreach ($this->PageAnnots as $p => $anno) { if ($p > $end_page) { foreach ($anno as $o) { - $newarr[($p - $n_tod)][] = $o; + $newarr[($p - $deletedPagesCount)][] = $o; } } elseif ($p < $start_page) { $newarr[$p] = $this->PageAnnots[$p]; @@ -23923,7 +24052,7 @@ function DeletePages($start_page, $end_page = -1) // Update PageNumSubstitutions foreach ($this->PageNumSubstitutions as $k => $v) { if ($this->PageNumSubstitutions[$k]['from'] > $end_page) { - $this->PageNumSubstitutions[$k]['from'] -= $n_tod; + $this->PageNumSubstitutions[$k]['from'] -= $deletedPagesCount; } elseif ($this->PageNumSubstitutions[$k]['from'] < $start_page) { unset($this->PageNumSubstitutions[$k]); } @@ -24484,7 +24613,7 @@ function printcolumnbuffer() $th = ($sum_h * $i / $this->NbCol); foreach ($breaks as $bk => $val) { if ($val > $th) { - if (($val - $th) < ($th - $breaks[$bk - 1])) { + if (!$bk || ($val - $th) < ($th - $breaks[$bk - 1])) { $cbr[$i - 1] = $val; } else { $cbr[$i - 1] = $breaks[$bk - 1]; @@ -26027,7 +26156,7 @@ function purify_utf8($html, $lo = true) { if (!$this->is_utf8($html)) { - while (mb_convert_encoding(mb_convert_encoding($html, "UTF-32", "UTF-8"), "UTF-8", "UTF-32") != $html) { + while (mb_convert_encoding(mb_convert_encoding($html, "UTF-32", "UTF-8"), "UTF-8", "UTF-32") !== $html) { $a = @iconv('UTF-8', 'UTF-8', $html); $error = error_get_last(); @@ -26961,7 +27090,7 @@ function AdjustHTML($html, $tabSpaces = 8) if (strlen($html) > (int) $limit) { throw new \Mpdf\MpdfException(sprintf( - 'The HTML code size is larger than pcre.backtrack_limit %d. You should use WriteHTML() with smaller string lengths.', + 'The HTML code size is larger than pcre.backtrack_limit %d. You should use WriteHTML() with smaller string lengths. Pass your HTML in smaller chunks.', $limit )); } @@ -26969,7 +27098,7 @@ function AdjustHTML($html, $tabSpaces = 8) preg_match_all("/()/si", $html, $m); if (count($m[1])) { for ($i = 0; $i < count($m[1]); $i++) { - $sub = preg_replace("/\n/si", "\xbb\xa4\xac", $m[1][$i]); + $sub = preg_replace("/\n/si", Mpdf::OBJECT_IDENTIFIER, $m[1][$i]); $html = preg_replace('/' . preg_quote($m[1][$i], '/') . '/si', $sub, $html); } } @@ -27107,8 +27236,16 @@ function AdjustHTML($html, $tabSpaces = 8) $html = preg_replace('/]*)><\/textarea>/si', ' ', $html); $html = preg_replace('/(]*>)\s*()(.*?<\/table>)/si', '\\2 position="top"\\3\\1\\4\\2 position="bottom"\\3', $html); // *TABLES* - $html = preg_replace('/<(h[1-6])([^>]*)(>(?:(?!h[1-6]).)*?<\/\\1>\s*use_kwt) { + $returnHtml = preg_replace('/<(h[1-6])([^>]*(?[^>]*<\/\\1>\s*

    which browser copes with even though it is wrong! $html = preg_replace("/(&#[x]{0,1}[0-9a-f]{1,5})OTLdata[$ptr + 1])) { + continue; + } + $nextGlyph = $this->OTLdata[$ptr + 1]['hex']; $nextGID = $this->OTLdata[$ptr + 1]['uni']; if (isset($this->GSLuCoverage[$lu][$c][$nextGID])) { @@ -5567,29 +5571,39 @@ public function bidiPrepare(&$para, $dir) */ public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) { - $bidiData = []; // First combine into one array (and get the highest level in use) $numchunks = count($content); $maxlevel = 0; + for ($nc = 0; $nc < $numchunks; $nc++) { + $numchars = isset($cOTLdata[$nc]['char_data']) ? count($cOTLdata[$nc]['char_data']) : 0; for ($i = 0; $i < $numchars; ++$i) { - $carac = []; + + $carac = [ + 'level' => 0, + ]; + if (isset($cOTLdata[$nc]['GPOSinfo'][$i])) { $carac['GPOSinfo'] = $cOTLdata[$nc]['GPOSinfo'][$i]; } + $carac['uni'] = $cOTLdata[$nc]['char_data'][$i]['uni']; + if (isset($cOTLdata[$nc]['char_data'][$i]['type'])) { $carac['type'] = $cOTLdata[$nc]['char_data'][$i]['type']; } + if (isset($cOTLdata[$nc]['char_data'][$i]['level'])) { $carac['level'] = $cOTLdata[$nc]['char_data'][$i]['level']; } + if (isset($cOTLdata[$nc]['char_data'][$i]['orig_type'])) { $carac['orig_type'] = $cOTLdata[$nc]['char_data'][$i]['orig_type']; } + $carac['group'] = $cOTLdata[$nc]['group'][$i]; $carac['chunkid'] = $chunkorder[$nc]; // gives font id and/or object ID @@ -5597,7 +5611,7 @@ public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) $bidiData[] = $carac; } } - if ($maxlevel == 0) { + if ($maxlevel === 0) { return; } @@ -5611,7 +5625,7 @@ public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) // The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2******* // Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line. // Set the initial paragraph embedding level - if ($blockdir == 'rtl') { + if ($blockdir === 'rtl') { $pel = 1; } else { $pel = 0; @@ -5631,6 +5645,7 @@ public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) $revarr = []; $onlevel = false; for ($i = 0; $i < $numchars; ++$i) { + if ($bidiData[$i]['level'] >= $j) { $onlevel = true; // L4. A character is depicted by a mirrored glyph if and only if (a) the resolved directionality of that character is R, and (b) the Bidi_Mirrored property value of that character is true. @@ -5639,20 +5654,25 @@ public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) } $revarr[] = $bidiData[$i]; + } else { + if ($onlevel) { $revarr = array_reverse($revarr); $ordarray = array_merge($ordarray, $revarr); $revarr = []; $onlevel = false; } + $ordarray[] = $bidiData[$i]; } } + if ($onlevel) { $revarr = array_reverse($revarr); $ordarray = array_merge($ordarray, $revarr); } + $bidiData = $ordarray; } diff --git a/vendor/mpdf/mpdf/src/PageBox.php b/vendor/mpdf/mpdf/src/PageBox.php new file mode 100644 index 00000000..773c23e6 --- /dev/null +++ b/vendor/mpdf/mpdf/src/PageBox.php @@ -0,0 +1,56 @@ +container = [ + 'current' => null, + 'outer_width_LR' => null, + 'outer_width_TB' => null, + 'using' => null, + ]; + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (!$this->offsetExists($offset)) { + throw new \Mpdf\MpdfException('Invalid key to set for PageBox'); + } + + $this->container[$offset] = $value; + } + + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return array_key_exists($offset, $this->container); + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + if (!$this->offsetExists($offset)) { + throw new \Mpdf\MpdfException('Invalid key to set for PageBox'); + } + + $this->container[$offset] = null; + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + if (!$this->offsetExists($offset)) { + throw new \Mpdf\MpdfException('Invalid key to set for PageBox'); + } + + return $this->container[$offset]; + } + +} diff --git a/vendor/mpdf/mpdf/src/ServiceFactory.php b/vendor/mpdf/mpdf/src/ServiceFactory.php index 1e776346..79633e7e 100644 --- a/vendor/mpdf/mpdf/src/ServiceFactory.php +++ b/vendor/mpdf/mpdf/src/ServiceFactory.php @@ -5,9 +5,12 @@ use Mpdf\Color\ColorConverter; use Mpdf\Color\ColorModeConverter; use Mpdf\Color\ColorSpaceRestrictor; +use Mpdf\File\LocalContentLoader; use Mpdf\Fonts\FontCache; use Mpdf\Fonts\FontFileFinder; -use \dokuwiki\plugin\dw2pdf\DokuImageProcessorDecorator as ImageProcessor; +use Mpdf\Http\CurlHttpClient; +use Mpdf\Http\SocketHttpClient; +use Mpdf\Image\ImageProcessor; use Mpdf\Pdf\Protection; use Mpdf\Pdf\Protection\UniqidGenerator; use Mpdf\Writer\BaseWriter; @@ -27,11 +30,20 @@ class ServiceFactory { + /** + * @var \Mpdf\Container\ContainerInterface|null + */ + private $container; + + public function __construct($container = null) + { + $this->container = $container; + } + public function getServices( Mpdf $mpdf, LoggerInterface $logger, $config, - $restrictColorSpace, $languageToFont, $scriptToLanguage, $fontDescriptor, @@ -44,8 +56,7 @@ public function getServices( $colorModeConverter = new ColorModeConverter(); $colorSpaceRestrictor = new ColorSpaceRestrictor( $mpdf, - $colorModeConverter, - $restrictColorSpace + $colorModeConverter ); $colorConverter = new ColorConverter($mpdf, $colorModeConverter, $colorSpaceRestrictor); @@ -58,9 +69,21 @@ public function getServices( $fontFileFinder = new FontFileFinder($config['fontDir']); - $remoteContentFetcher = new RemoteContentFetcher($mpdf, $logger); + if ($this->container && $this->container->has('httpClient')) { + $httpClient = $this->container->get('httpClient'); + } elseif (\function_exists('curl_init')) { + $httpClient = new CurlHttpClient($mpdf, $logger); + } else { + $httpClient = new SocketHttpClient($logger); + } + + $localContentLoader = $this->container && $this->container->has('localContentLoader') + ? $this->container->get('localContentLoader') + : new LocalContentLoader(); + + $assetFetcher = new AssetFetcher($mpdf, $localContentLoader, $httpClient, $logger); - $cssManager = new CssManager($mpdf, $cache, $sizeConverter, $colorConverter, $remoteContentFetcher); + $cssManager = new CssManager($mpdf, $cache, $sizeConverter, $colorConverter, $assetFetcher); $otl = new Otl($mpdf, $fontCache); @@ -86,7 +109,7 @@ public function getServices( $cache, $languageToFont, $scriptToLanguage, - $remoteContentFetcher, + $assetFetcher, $logger ); @@ -144,7 +167,9 @@ public function getServices( 'sizeConverter' => $sizeConverter, 'colorConverter' => $colorConverter, 'hyphenator' => $hyphenator, - 'remoteContentFetcher' => $remoteContentFetcher, + 'localContentLoader' => $localContentLoader, + 'httpClient' => $httpClient, + 'assetFetcher' => $assetFetcher, 'imageProcessor' => $imageProcessor, 'protection' => $protection, @@ -162,9 +187,48 @@ public function getServices( 'colorWriter' => $colorWriter, 'backgroundWriter' => $backgroundWriter, 'javaScriptWriter' => $javaScriptWriter, - 'resourceWriter' => $resourceWriter ]; } + public function getServiceIds() + { + return [ + 'otl', + 'bmp', + 'cache', + 'cssManager', + 'directWrite', + 'fontCache', + 'fontFileFinder', + 'form', + 'gradient', + 'tableOfContents', + 'tag', + 'wmf', + 'sizeConverter', + 'colorConverter', + 'hyphenator', + 'localContentLoader', + 'httpClient', + 'assetFetcher', + 'imageProcessor', + 'protection', + 'languageToFont', + 'scriptToLanguage', + 'writer', + 'fontWriter', + 'metadataWriter', + 'imageWriter', + 'formWriter', + 'pageWriter', + 'bookmarkWriter', + 'optionalContentWriter', + 'colorWriter', + 'backgroundWriter', + 'javaScriptWriter', + 'resourceWriter', + ]; + } + } diff --git a/vendor/mpdf/mpdf/src/SizeConverter.php b/vendor/mpdf/mpdf/src/SizeConverter.php index 9ec93fd2..15a51873 100644 --- a/vendor/mpdf/mpdf/src/SizeConverter.php +++ b/vendor/mpdf/mpdf/src/SizeConverter.php @@ -4,10 +4,13 @@ use Psr\Log\LoggerInterface; use Mpdf\Log\Context as LogContext; +use Mpdf\PsrLogAwareTrait\PsrLogAwareTrait; class SizeConverter implements \Psr\Log\LoggerAwareInterface { + use PsrLogAwareTrait; + private $dpi; private $defaultFontSize; @@ -17,11 +20,6 @@ class SizeConverter implements \Psr\Log\LoggerAwareInterface */ private $mpdf; - /** - * @var \Psr\Log\LoggerInterface - */ - private $logger; - public function __construct($dpi, $defaultFontSize, Mpdf $mpdf, LoggerInterface $logger) { $this->dpi = $dpi; @@ -30,11 +28,6 @@ public function __construct($dpi, $defaultFontSize, Mpdf $mpdf, LoggerInterface $this->logger = $logger; } - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - /** * Depends of maxsize value to make % work properly. Usually maxsize == pagewidth * For text $maxsize = $fontsize @@ -77,6 +70,7 @@ public function convert($size = 5, $maxsize = 0, $fontsize = false, $usefontsize break; case '%': + case '%%': // Issue2051 if ($fontsize && $usefontsize) { $size *= $fontsize / 100; } else { diff --git a/vendor/mpdf/mpdf/src/TTFontFile.php b/vendor/mpdf/mpdf/src/TTFontFile.php index 642699b5..2591bdbb 100644 --- a/vendor/mpdf/mpdf/src/TTFontFile.php +++ b/vendor/mpdf/mpdf/src/TTFontFile.php @@ -2297,7 +2297,7 @@ function _getGSUBtables() $key = $vs['match'][1]; $tag = $v['tag']; if (isset($loclsubs[$key])) { - ${$tag[$loclsubs[$key]]} = $sub; + ${$tag}[$loclsubs[$key]] = $sub; } $tmp = &$$tag; $tmp[hexdec($key)] = hexdec($sub); @@ -2307,7 +2307,7 @@ function _getGSUBtables() $key = $vs['match'][0]; $tag = $v['tag']; if (isset($loclsubs[$key])) { - ${$tag[$loclsubs[$key]]} = $sub; + ${$tag}[$loclsubs[$key]] = $sub; } $tmp = &$$tag; $tmp[hexdec($key)] = hexdec($sub); @@ -2326,7 +2326,7 @@ function _getGSUBtables() $key = substr($key, 6, 5); $tag = $v['tag']; if (isset($loclsubs[$key])) { - ${$tag[$loclsubs[$key]]} = $sub; + ${$tag}[$loclsubs[$key]] = $sub; } $tmp = &$$tag; $tmp[hexdec($key)] = hexdec($sub); @@ -2334,7 +2334,7 @@ function _getGSUBtables() $key = substr($key, 0, 5); $tag = $v['tag']; if (isset($loclsubs[$key])) { - ${$tag[$loclsubs[$key]]} = $sub; + ${$tag}[$loclsubs[$key]] = $sub; } $tmp = &$$tag; $tmp[hexdec($key)] = hexdec($sub); @@ -2888,27 +2888,28 @@ function _getGSUBarray(&$Lookup, &$lul, $scripttag) $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { - if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { - foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { - $lookupGlyphs = $luss['Replace']; - $mLen = count($lookupGlyphs); + if (empty($Lookup[$lup]['Subtable'][$lus]['subs']) || ! is_array($Lookup[$lup]['Subtable'][$lus]['subs'])) { + continue; + } - // Only apply if the (first) 'Replace' glyph from the - // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] - // then apply the substitution - if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { - continue; - } + foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { + $lookupGlyphs = $luss['Replace']; - // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ - $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); - $REPL = implode(" ", $luss['substitute']); + // Only apply if the (first) 'Replace' glyph from the + // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] + // then apply the substitution + if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { + continue; + } - if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { - $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; - } else { - $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; - } + // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ + $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); + $REPL = implode(" ", $luss['substitute']); + + if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { + $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; + } else { + $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; } } } @@ -2972,7 +2973,7 @@ function _getGSUBignoreString($flag, $MarkFilteringSet) // Flag & 0x0010 = UseMarkFilteringSet if ($flag & 0x0010) { - throw new \Mpdf\Exception\FontException("This font " . $this->fontkey . " contains MarkGlyphSets - Not tested yet"); + throw new \Mpdf\Exception\FontException("Font \"" . $this->fontkey . "\" contains MarkGlyphSets which is not supported"); $str = $this->MarkGlyphSets[$MarkFilteringSet]; } diff --git a/vendor/mpdf/mpdf/src/Tag.php b/vendor/mpdf/mpdf/src/Tag.php index a9081413..33d90b6b 100644 --- a/vendor/mpdf/mpdf/src/Tag.php +++ b/vendor/mpdf/mpdf/src/Tag.php @@ -4,7 +4,7 @@ use Mpdf\Strict; use Mpdf\Color\ColorConverter; -use \dokuwiki\plugin\dw2pdf\DokuImageProcessorDecorator as ImageProcessor; +use Mpdf\Image\ImageProcessor; use Mpdf\Language\LanguageToFontInterface; class Tag @@ -53,7 +53,7 @@ class Tag private $colorConverter; /** - * @var ImageProcessor + * @var \Mpdf\Image\ImageProcessor */ private $imageProcessor; diff --git a/vendor/mpdf/mpdf/src/Tag/A.php b/vendor/mpdf/mpdf/src/Tag/A.php index 6a6488b1..0853b239 100644 --- a/vendor/mpdf/mpdf/src/Tag/A.php +++ b/vendor/mpdf/mpdf/src/Tag/A.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class A extends Tag { @@ -19,7 +21,7 @@ public function open($attr, &$ahtml, &$ihtml) } else { $objattr['bklevel'] = 0; } - $e = "\xbb\xa4\xactype=bookmark,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=bookmark,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; } /* -- END BOOKMARKS -- */ if ($this->mpdf->tableLevel) { // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/Annotation.php b/vendor/mpdf/mpdf/src/Tag/Annotation.php index 6ed5e0e5..f156fc6f 100644 --- a/vendor/mpdf/mpdf/src/Tag/Annotation.php +++ b/vendor/mpdf/mpdf/src/Tag/Annotation.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class Annotation extends Tag { @@ -84,7 +86,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['POPUP'] = true; } } - $e = "\xbb\xa4\xactype=annot,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=annot,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['textbuffer'][] = [$e]; } // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/BarCode.php b/vendor/mpdf/mpdf/src/Tag/BarCode.php index 44ecd951..40bf5dc4 100644 --- a/vendor/mpdf/mpdf/src/Tag/BarCode.php +++ b/vendor/mpdf/mpdf/src/Tag/BarCode.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class BarCode extends Tag { @@ -31,7 +33,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['border_bottom']['w'] = 0; $objattr['border_left']['w'] = 0; $objattr['border_right']['w'] = 0; - $objattr['code'] = $attr['CODE']; + $objattr['code'] = htmlspecialchars_decode($attr['CODE']); if (isset($attr['TYPE'])) { $objattr['btype'] = strtoupper(trim($attr['TYPE'])); @@ -249,7 +251,7 @@ public function open($attr, &$ahtml, &$ihtml) } /* -- END CSS-IMAGE-FLOAT -- */ - $e = "\xbb\xa4\xactype=barcode,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=barcode,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; /* -- TABLES -- */ // Output it to buffers diff --git a/vendor/mpdf/mpdf/src/Tag/BlockTag.php b/vendor/mpdf/mpdf/src/Tag/BlockTag.php index 94a17dc1..7f2ba763 100644 --- a/vendor/mpdf/mpdf/src/Tag/BlockTag.php +++ b/vendor/mpdf/mpdf/src/Tag/BlockTag.php @@ -4,6 +4,7 @@ use Mpdf\Conversion\DecToAlpha; use Mpdf\Conversion\DecToRoman; +use Mpdf\Mpdf; use Mpdf\Utils\Arrays; use Mpdf\Utils\UtfString; @@ -246,11 +247,9 @@ public function open($attr, &$ahtml, &$ihtml) /* -- END CSS-PAGE -- */ // If page-box has changed AND/OR PAGE-BREAK-BEFORE - // mPDF 6 (uses $p - preview of properties so blklvl can be imcremented after page-break) - if (!$this->mpdf->tableLevel && (($pagesel && (!isset($this->mpdf->page_box['current']) - || $pagesel != $this->mpdf->page_box['current'])) - || (isset($p['PAGE-BREAK-BEFORE']) - && $p['PAGE-BREAK-BEFORE']))) { + // mPDF 6 (uses $p - preview of properties so blklvl can be incremented after page-break) + if (!$this->mpdf->tableLevel && (($pagesel && (!$this->mpdf->page_box['current'] || $pagesel != $this->mpdf->page_box['current'])) + || (isset($p['PAGE-BREAK-BEFORE']) && $p['PAGE-BREAK-BEFORE']))) { // mPDF 6 pagebreaktype $startpage = $this->mpdf->page; $pagebreaktype = $this->mpdf->defaultPagebreakType; @@ -258,7 +257,7 @@ public function open($attr, &$ahtml, &$ihtml) if ($this->mpdf->ColActive) { $pagebreaktype = 'cloneall'; } - if ($pagesel && (!isset($this->mpdf->page_box['current']) || $pagesel != $this->mpdf->page_box['current'])) { + if ($pagesel && (!$this->mpdf->page_box['current'] || $pagesel != $this->mpdf->page_box['current'])) { $pagebreaktype = 'cloneall'; } $this->mpdf->_preForcedPagebreak($pagebreaktype); @@ -317,7 +316,7 @@ public function open($attr, &$ahtml, &$ihtml) } // *CSS-PAGE* } /* -- CSS-PAGE -- */ // Must Add new page if changed page properties - elseif (!isset($this->mpdf->page_box['current']) || $pagesel != $this->mpdf->page_box['current']) { + elseif (!$this->mpdf->page_box['current'] || $pagesel != $this->mpdf->page_box['current']) { $this->mpdf->AddPage($this->mpdf->CurOrientation, '', '', '', '', '', '', '', '', '', '', '', '', '', '', 0, 0, 0, 0, $pagesel); } /* -- END CSS-PAGE -- */ @@ -992,7 +991,7 @@ public function close(&$ahtml, &$ihtml) $content = $this->mpdf->textbuffer[0][0]; } else { for ($i = 0; $i < count($this->mpdf->textbuffer); $i++) { - if (0 !== strpos($this->mpdf->textbuffer[$i][0], "\xbb\xa4\xac")) { //inline object + if (0 !== strpos($this->mpdf->textbuffer[$i][0], Mpdf::OBJECT_IDENTIFIER)) { //inline object $content .= $this->mpdf->textbuffer[$i][0]; } } @@ -1003,7 +1002,7 @@ public function close(&$ahtml, &$ihtml) $objattr['type'] = 'toc'; $objattr['toclevel'] = $this->mpdf->h2toc[$tag]; $objattr['CONTENT'] = htmlspecialchars($content); - $e = "\xbb\xa4\xactype=toc,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=toc,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; array_unshift($this->mpdf->textbuffer, [$e]); } /* -- END TOC -- */ @@ -1013,7 +1012,7 @@ public function close(&$ahtml, &$ihtml) $objattr['type'] = 'bookmark'; $objattr['bklevel'] = $this->mpdf->h2bookmarks[$tag]; $objattr['CONTENT'] = $content; - $e = "\xbb\xa4\xactype=toc,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=toc,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; array_unshift($this->mpdf->textbuffer, [$e]); } /* -- END BOOKMARKS -- */ @@ -1081,7 +1080,7 @@ public function close(&$ahtml, &$ihtml) // called from after e.g. ... Outputs block margin/border and padding if (count($this->mpdf->textbuffer) && $this->mpdf->textbuffer[count($this->mpdf->textbuffer) - 1]) { - if (0 !== strpos($this->mpdf->textbuffer[count($this->mpdf->textbuffer) - 1][0], "\xbb\xa4\xac")) { // not special content + if (0 !== strpos($this->mpdf->textbuffer[count($this->mpdf->textbuffer) - 1][0], Mpdf::OBJECT_IDENTIFIER)) { // not special content // Right trim last content and adjust OTLdata if (preg_match('/[ ]+$/', $this->mpdf->textbuffer[count($this->mpdf->textbuffer) - 1][0], $m)) { $strip = strlen($m[0]); diff --git a/vendor/mpdf/mpdf/src/Tag/Bookmark.php b/vendor/mpdf/mpdf/src/Tag/Bookmark.php index b414cf4c..47b6907c 100644 --- a/vendor/mpdf/mpdf/src/Tag/Bookmark.php +++ b/vendor/mpdf/mpdf/src/Tag/Bookmark.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class Bookmark extends Tag { @@ -16,7 +18,7 @@ public function open($attr, &$ahtml, &$ihtml) } else { $objattr['bklevel'] = 0; } - $e = "\xbb\xa4\xactype=bookmark,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=bookmark,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['textbuffer'][] = [$e]; } // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/DotTab.php b/vendor/mpdf/mpdf/src/Tag/DotTab.php index 278ce38b..0537a909 100644 --- a/vendor/mpdf/mpdf/src/Tag/DotTab.php +++ b/vendor/mpdf/mpdf/src/Tag/DotTab.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class DotTab extends Tag { @@ -45,7 +47,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['fontfamily'] = $this->mpdf->FontFamily; $objattr['fontsize'] = $this->mpdf->FontSizePt; - $e = "\xbb\xa4\xactype=dottab,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=dottab,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; /* -- TABLES -- */ // Output it to buffers if ($this->mpdf->tableLevel) { diff --git a/vendor/mpdf/mpdf/src/Tag/Hr.php b/vendor/mpdf/mpdf/src/Tag/Hr.php index 89c880b9..8d023f2a 100644 --- a/vendor/mpdf/mpdf/src/Tag/Hr.php +++ b/vendor/mpdf/mpdf/src/Tag/Hr.php @@ -2,6 +2,7 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; use Mpdf\Utils\NumericString; class Hr extends Tag @@ -100,7 +101,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['type'] = 'hr'; $objattr['height'] = $objattr['linewidth'] + $objattr['margin_top'] + $objattr['margin_bottom']; - $e = "\xbb\xa4\xactype=image,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=image,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; /* -- TABLES -- */ // Output it to buffers diff --git a/vendor/mpdf/mpdf/src/Tag/Img.php b/vendor/mpdf/mpdf/src/Tag/Img.php index 30b357d0..efd3ed94 100644 --- a/vendor/mpdf/mpdf/src/Tag/Img.php +++ b/vendor/mpdf/mpdf/src/Tag/Img.php @@ -1,7 +1,6 @@ mpdf->annotOpacity; $objattr['COLOR'] = $this->colorConverter->convert('yellow', $this->mpdf->PDFAXwarnings); - $e = "\xbb\xa4\xactype=annot,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=annot,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { // *TABLES* $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['textbuffer'][] = [$e]; // *TABLES* } // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/IndexEntry.php b/vendor/mpdf/mpdf/src/Tag/IndexEntry.php index 2ca06a4e..01008594 100644 --- a/vendor/mpdf/mpdf/src/Tag/IndexEntry.php +++ b/vendor/mpdf/mpdf/src/Tag/IndexEntry.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class IndexEntry extends Tag { @@ -16,7 +18,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['CONTENT'] = htmlspecialchars_decode($attr['CONTENT'], ENT_QUOTES); $objattr['type'] = 'indexentry'; $objattr['vertical-align'] = 'T'; - $e = "\xbb\xa4\xactype=indexentry,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=indexentry,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['textbuffer'][] = [$e]; } // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/InlineTag.php b/vendor/mpdf/mpdf/src/Tag/InlineTag.php index 69ea5f56..c599f1c6 100644 --- a/vendor/mpdf/mpdf/src/Tag/InlineTag.php +++ b/vendor/mpdf/mpdf/src/Tag/InlineTag.php @@ -2,6 +2,7 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; use Mpdf\Utils\UtfString; abstract class InlineTag extends Tag @@ -34,7 +35,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['SUBJECT'] = ''; $objattr['OPACITY'] = $this->mpdf->annotOpacity; $objattr['COLOR'] = $this->colorConverter->convert('yellow', $this->mpdf->PDFAXwarnings); - $annot = "\xbb\xa4\xactype=annot,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $annot = Mpdf::OBJECT_IDENTIFIER . "type=annot,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; } /* -- END ANNOTATIONS -- */ diff --git a/vendor/mpdf/mpdf/src/Tag/Input.php b/vendor/mpdf/mpdf/src/Tag/Input.php index d98039c1..bda8bc50 100644 --- a/vendor/mpdf/mpdf/src/Tag/Input.php +++ b/vendor/mpdf/mpdf/src/Tag/Input.php @@ -70,7 +70,7 @@ public function open($attr, &$ahtml, &$ihtml) if (isset($properties['FONT-FAMILY'])) { $this->mpdf->SetFont($properties['FONT-FAMILY'], $this->mpdf->FontStyle, 0, false); } - if (isset($properties['FONT-SIZE'])) { + if (isset($properties['FONT-SIZE']) && $properties['FONT-SIZE'] !== 'auto') { $mmsize = $this->sizeConverter->convert($properties['FONT-SIZE'], $this->mpdf->default_font_size / Mpdf::SCALE); $this->mpdf->SetFontSize($mmsize * Mpdf::SCALE, false); } @@ -356,6 +356,11 @@ public function open($attr, &$ahtml, &$ihtml) if (strtoupper($attr['TYPE']) === 'PASSWORD') { $type = 'PASSWORD'; } + + if ($properties['FONT-SIZE'] === 'auto' && $this->mpdf->useActiveForms) { + $objattr['use_auto_fontsize'] = true; + } + if (isset($attr['VALUE'])) { if ($type === 'PASSWORD') { $num_stars = mb_strlen($attr['VALUE'], $this->mpdf->mb_enc); @@ -401,7 +406,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['text'] = $texto; $objattr['width'] = $width; $objattr['height'] = $height; - $e = "\xbb\xa4\xactype=input,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=input,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; /* -- TABLES -- */ // Output it to buffers diff --git a/vendor/mpdf/mpdf/src/Tag/Meter.php b/vendor/mpdf/mpdf/src/Tag/Meter.php index 2ffff3d0..a99e8f89 100644 --- a/vendor/mpdf/mpdf/src/Tag/Meter.php +++ b/vendor/mpdf/mpdf/src/Tag/Meter.php @@ -285,7 +285,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['width'] = $w + $extrawidth; $objattr['image_height'] = $h; $objattr['image_width'] = $w; - $e = "\xbb\xa4\xactype=image,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=image,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { $this->mpdf->_saveCellTextBuffer($e, $this->mpdf->HREF); $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $objattr['width']; diff --git a/vendor/mpdf/mpdf/src/Tag/Option.php b/vendor/mpdf/mpdf/src/Tag/Option.php index c5233d1d..04221d0f 100644 --- a/vendor/mpdf/mpdf/src/Tag/Option.php +++ b/vendor/mpdf/mpdf/src/Tag/Option.php @@ -27,7 +27,8 @@ public function open($attr, &$ahtml, &$ihtml) $attr['VALUE'] = mb_convert_encoding($attr['VALUE'], $this->mpdf->mb_enc, 'UTF-8'); } } - $this->mpdf->selectoption['currentVAL'] = $attr['VALUE']; + + $this->mpdf->selectoption['currentVAL'] = isset($attr['VALUE']) ? $attr['VALUE'] : $ahtml[$ihtml + 1]; } public function close(&$ahtml, &$ihtml) diff --git a/vendor/mpdf/mpdf/src/Tag/Select.php b/vendor/mpdf/mpdf/src/Tag/Select.php index 1d3c131a..60addcb7 100644 --- a/vendor/mpdf/mpdf/src/Tag/Select.php +++ b/vendor/mpdf/mpdf/src/Tag/Select.php @@ -132,7 +132,7 @@ public function close(&$ahtml, &$ihtml) $objattr['height'] = ($this->mpdf->FontSize * $rows) + ($this->form->form_element_spacing['select']['outer']['v'] * 2) + ($this->form->form_element_spacing['select']['inner']['v'] * 2); - $e = "\xbb\xa4\xactype=select,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=select,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; // Output it to buffers if ($this->mpdf->tableLevel) { // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/Table.php b/vendor/mpdf/mpdf/src/Tag/Table.php index 6a7b427c..9ed219c6 100644 --- a/vendor/mpdf/mpdf/src/Tag/Table.php +++ b/vendor/mpdf/mpdf/src/Tag/Table.php @@ -712,7 +712,7 @@ public function close(&$ahtml, &$ihtml) $objattr['row'] = $this->mpdf->row; $objattr['col'] = $this->mpdf->col; $objattr['level'] = $this->mpdf->tableLevel; - $e = "\xbb\xa4\xactype=nestedtable,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=nestedtable,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; $this->mpdf->_saveCellTextBuffer($e); $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $tl; if (!isset($this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['maxs'])) { diff --git a/vendor/mpdf/mpdf/src/Tag/Tag.php b/vendor/mpdf/mpdf/src/Tag/Tag.php index a75438a4..094b2a46 100644 --- a/vendor/mpdf/mpdf/src/Tag/Tag.php +++ b/vendor/mpdf/mpdf/src/Tag/Tag.php @@ -8,7 +8,7 @@ use Mpdf\Color\ColorConverter; use Mpdf\CssManager; use Mpdf\Form; -use \dokuwiki\plugin\dw2pdf\DokuImageProcessorDecorator as ImageProcessor; +use Mpdf\Image\ImageProcessor; use Mpdf\Language\LanguageToFontInterface; use Mpdf\Mpdf; use Mpdf\Otl; @@ -61,7 +61,7 @@ abstract class Tag protected $colorConverter; /** - * @var ImageProcessor + * @var \Mpdf\Image\ImageProcessor */ protected $imageProcessor; diff --git a/vendor/mpdf/mpdf/src/Tag/Td.php b/vendor/mpdf/mpdf/src/Tag/Td.php index d74cc0b8..5f82fab7 100644 --- a/vendor/mpdf/mpdf/src/Tag/Td.php +++ b/vendor/mpdf/mpdf/src/Tag/Td.php @@ -420,7 +420,7 @@ public function open($attr, &$ahtml, &$ihtml) } } - if (isset($attr['ROWSPAN']) && $attr['ROWSPAN'] > 1) { + if (isset($attr['ROWSPAN']) && preg_match('/^\d+$/', $attr['ROWSPAN']) && $attr['ROWSPAN'] > 1) { $rs = $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['rowspan'] = $attr['ROWSPAN']; } diff --git a/vendor/mpdf/mpdf/src/Tag/TextArea.php b/vendor/mpdf/mpdf/src/Tag/TextArea.php index 017c5484..46539d8b 100644 --- a/vendor/mpdf/mpdf/src/Tag/TextArea.php +++ b/vendor/mpdf/mpdf/src/Tag/TextArea.php @@ -64,7 +64,7 @@ public function open($attr, &$ahtml, &$ihtml) if (isset($properties['FONT-FAMILY'])) { $this->mpdf->SetFont($properties['FONT-FAMILY'], '', 0, false); } - if (isset($properties['FONT-SIZE'])) { + if (isset($properties['FONT-SIZE']) && $properties['FONT-SIZE'] !== 'auto') { $mmsize = $this->sizeConverter->convert($properties['FONT-SIZE'], $this->mpdf->default_font_size / Mpdf::SCALE); $this->mpdf->SetFontSize($mmsize * Mpdf::SCALE, false); } @@ -143,6 +143,10 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['rows'] = $rowsize; $objattr['cols'] = $colsize; + if ($properties['FONT-SIZE'] === 'auto' && $this->mpdf->useActiveForms) { + $objattr['use_auto_fontsize'] = true; + } + $this->mpdf->specialcontent = serialize($objattr); if ($this->mpdf->tableLevel) { // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/TextCircle.php b/vendor/mpdf/mpdf/src/Tag/TextCircle.php index 6bd53133..7d321441 100644 --- a/vendor/mpdf/mpdf/src/Tag/TextCircle.php +++ b/vendor/mpdf/mpdf/src/Tag/TextCircle.php @@ -226,7 +226,7 @@ public function open($attr, &$ahtml, &$ihtml) $objattr['width'] = $w + $extrawidth; $objattr['type'] = 'textcircle'; - $e = "\xbb\xa4\xactype=image,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=image,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; /* -- TABLES -- */ // Output it to buffers diff --git a/vendor/mpdf/mpdf/src/Tag/TocEntry.php b/vendor/mpdf/mpdf/src/Tag/TocEntry.php index 1b315abe..2ba6e693 100644 --- a/vendor/mpdf/mpdf/src/Tag/TocEntry.php +++ b/vendor/mpdf/mpdf/src/Tag/TocEntry.php @@ -2,6 +2,8 @@ namespace Mpdf\Tag; +use Mpdf\Mpdf; + class TocEntry extends Tag { @@ -22,7 +24,7 @@ public function open($attr, &$ahtml, &$ihtml) } else { $objattr['toc_id'] = 0; } - $e = "\xbb\xa4\xactype=toc,objattr=" . serialize($objattr) . "\xbb\xa4\xac"; + $e = Mpdf::OBJECT_IDENTIFIER . "type=toc,objattr=" . serialize($objattr) . Mpdf::OBJECT_IDENTIFIER; if ($this->mpdf->tableLevel) { $this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['textbuffer'][] = [$e]; } // *TABLES* diff --git a/vendor/mpdf/mpdf/src/Tag/Tr.php b/vendor/mpdf/mpdf/src/Tag/Tr.php index 24b7749b..1e9bdc6b 100644 --- a/vendor/mpdf/mpdf/src/Tag/Tr.php +++ b/vendor/mpdf/mpdf/src/Tag/Tr.php @@ -17,6 +17,17 @@ public function open($attr, &$ahtml, &$ihtml) $this->mpdf->col = -1; $properties = $this->cssManager->MergeCSS('TABLE', 'TR', $attr); + // write pagebreak markers into row list, so _tableWrite can respect it + if (isset($properties['PAGE-BREAK-BEFORE']) && strtoupper($properties['PAGE-BREAK-BEFORE']) === 'AVOID' + && !$this->mpdf->ColActive && !$this->mpdf->keep_block_together && !isset($attr['PAGEBREAKAVOIDCHECKED'])) { + $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['pagebreak-before'][$this->mpdf->row] = 'avoid'; + } + + if (isset($properties['PAGE-BREAK-AFTER']) && strtoupper($properties['PAGE-BREAK-AFTER']) === 'AVOID' + && !$this->mpdf->ColActive && !$this->mpdf->keep_block_together && !isset($attr['PAGEBREAKAVOIDCHECKED'])) { + $this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['pagebreak-before'][$this->mpdf->row + 1] = 'avoid'; + } + if (!$this->mpdf->simpleTables && (!isset($this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['borders_separate']) || !$this->mpdf->table[$this->mpdf->tableLevel][$this->mpdf->tbctr[$this->mpdf->tableLevel]]['borders_separate'])) { if (!empty($properties['BORDER-LEFT'])) { diff --git a/vendor/mpdf/mpdf/src/Utils/UtfString.php b/vendor/mpdf/mpdf/src/Utils/UtfString.php index 8b5c8c94..2bf9c573 100644 --- a/vendor/mpdf/mpdf/src/Utils/UtfString.php +++ b/vendor/mpdf/mpdf/src/Utils/UtfString.php @@ -36,17 +36,19 @@ public static function code2utf($num, $lo = true) { // Returns the utf string corresponding to the unicode value if ($num < 128) { - if ($lo) { - return chr($num); - } - return '&#' . $num . ';'; + return $lo + ? chr($num) + : '&#' . $num . ';'; } + if ($num < 2048) { return chr(($num >> 6) + 192) . chr(($num & 63) + 128); } + if ($num < 65536) { return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); } + if ($num < 2097152) { return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); } diff --git a/vendor/mpdf/mpdf/src/Watermark.php b/vendor/mpdf/mpdf/src/Watermark.php new file mode 100644 index 00000000..4d908de3 --- /dev/null +++ b/vendor/mpdf/mpdf/src/Watermark.php @@ -0,0 +1,8 @@ +path = $path; + $this->size = $size; + $this->position = $position; + $this->alpha = $alpha; + $this->behindContent = $behindContent; + $this->alphaBlend = $alphaBlend; + } + + public function getPath() + { + return $this->path; + } + + public function getSize() + { + return $this->size; + } + + public function getPosition() + { + return $this->position; + } + + public function getAlpha() + { + return $this->alpha; + } + + public function isBehindContent() + { + return $this->behindContent; + } + + public function getAlphaBlend() + { + return $this->alphaBlend; + } + +} diff --git a/vendor/mpdf/mpdf/src/WatermarkText.php b/vendor/mpdf/mpdf/src/WatermarkText.php new file mode 100644 index 00000000..1e1129b2 --- /dev/null +++ b/vendor/mpdf/mpdf/src/WatermarkText.php @@ -0,0 +1,66 @@ +text = $text; + $this->size = $size; + $this->angle = $angle; + $this->color = $color; + $this->alpha = $alpha; + $this->font = $font; + } + + public function getText() + { + return $this->text; + } + + public function getSize() + { + return $this->size; + } + + public function getAngle() + { + return $this->angle; + } + + public function getColor() + { + return $this->color; + } + + public function getAlpha() + { + return $this->alpha; + } + + public function getFont() + { + return $this->font; + } + +} diff --git a/vendor/mpdf/mpdf/src/Writer/FontWriter.php b/vendor/mpdf/mpdf/src/Writer/FontWriter.php index ddc70232..a78dcf70 100644 --- a/vendor/mpdf/mpdf/src/Writer/FontWriter.php +++ b/vendor/mpdf/mpdf/src/Writer/FontWriter.php @@ -46,7 +46,7 @@ public function writeFonts() // TrueType embedded if (isset($info['type']) && $info['type'] === 'TTF' && !$info['sip'] && !$info['smp']) { $used = true; - $asSubset = false; + $asSubset = true; foreach ($this->mpdf->fonts as $k => $f) { if (isset($f['fontkey']) && $f['fontkey'] === $fontkey && $f['type'] === 'TTF') { $used = $f['used']; @@ -61,9 +61,6 @@ public function writeFonts() $asSubset = true; } } - if ($this->mpdf->PDFA || $this->mpdf->PDFX) { - $asSubset = false; - } $this->mpdf->fonts[$k]['asSubset'] = $asSubset; break; } @@ -165,10 +162,16 @@ public function writeFonts() $ssfaid = 'AA'; $ttf = new TTFontFile($this->fontCache, $this->fontDescriptor); $subsetCount = count($font['subsetfontids']); + for ($sfid = 0; $sfid < $subsetCount; $sfid++) { $this->mpdf->fonts[$k]['n'][$sfid] = $this->mpdf->n + 1; // NB an array for subset $subsetname = 'MPDF' . $ssfaid . '+' . $font['name']; - $ssfaid++; + + if (function_exists('str_increment')) { + $ssfaid = str_increment($ssfaid); + } else { + $ssfaid++; + } /* For some strange reason a subset ($sfid > 0) containing less than 97 characters causes an error so fill up the array */ @@ -479,7 +482,10 @@ private function writeTTFontWidths(&$font, $asSubset, $maxUni) // _putTTfontwidt continue; } - $width = (ord($character1) << 8) + ord($character2); + $w1 = $character1 === '' ? 0 : ord($character1); + $w2 = $character2 === '' ? 0 : ord($character2); + + $width = ($w1 << 8) + $w2; if ($width === 65535) { $width = 0; diff --git a/vendor/mpdf/mpdf/src/Writer/MetadataWriter.php b/vendor/mpdf/mpdf/src/Writer/MetadataWriter.php index a171c732..f5954512 100644 --- a/vendor/mpdf/mpdf/src/Writer/MetadataWriter.php +++ b/vendor/mpdf/mpdf/src/Writer/MetadataWriter.php @@ -6,6 +6,7 @@ use Mpdf\Form; use Mpdf\Mpdf; use Mpdf\Pdf\Protection; +use Mpdf\PsrLogAwareTrait\PsrLogAwareTrait; use Mpdf\Utils\PdfDate; use Psr\Log\LoggerInterface; @@ -14,6 +15,7 @@ class MetadataWriter implements \Psr\Log\LoggerAwareInterface { use Strict; + use PsrLogAwareTrait; /** * @var \Mpdf\Mpdf @@ -35,11 +37,6 @@ class MetadataWriter implements \Psr\Log\LoggerAwareInterface */ private $protection; - /** - * @var \Psr\Log\LoggerInterface - */ - private $logger; - public function __construct(Mpdf $mpdf, BaseWriter $writer, Form $form, Protection $protection, LoggerInterface $logger) { $this->mpdf = $mpdf; @@ -53,20 +50,21 @@ public function writeMetadata() // _putmetadata { $this->writer->object(); $this->mpdf->MetadataRoot = $this->mpdf->n; - $Producer = 'mPDF' . ($this->mpdf->exposeVersion ? (' ' . Mpdf::VERSION) : ''); + $z = date('O'); // +0200 $offset = substr($z, 0, 3) . ':' . substr($z, 3, 2); + $CreationDate = date('Y-m-d\TH:i:s') . $offset; // 2006-03-10T10:47:26-05:00 2006-06-19T09:05:17Z - $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0x0fff) | 0x4000, random_int(0, 0x3fff) | 0x8000, random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)); + $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0x0fff) | 0x4000, random_int(0, 0x3fff) | 0x8000, random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)); $m = '' . "\n"; // begin = FEFF BOM $m .= ' ' . "\n"; $m .= ' ' . "\n"; $m .= ' ' . "\n"; - $m .= ' ' . $Producer . '' . "\n"; + $m .= ' ' . htmlspecialchars($this->getProducerString(), ENT_QUOTES | ENT_XML1) . '' . "\n"; if (!empty($this->mpdf->keywords)) { - $m .= ' ' . $this->mpdf->keywords . '' . "\n"; + $m .= ' ' . htmlspecialchars($this->mpdf->keywords, ENT_QUOTES | ENT_XML1) . '' . "\n"; } $m .= ' ' . "\n"; @@ -75,7 +73,7 @@ public function writeMetadata() // _putmetadata $m .= ' ' . $CreationDate . '' . "\n"; $m .= ' ' . $CreationDate . '' . "\n"; if (!empty($this->mpdf->creator)) { - $m .= ' ' . $this->mpdf->creator . '' . "\n"; + $m .= ' ' . htmlspecialchars($this->mpdf->creator, ENT_QUOTES | ENT_XML1) . '' . "\n"; } $m .= ' ' . "\n"; @@ -85,28 +83,28 @@ public function writeMetadata() // _putmetadata if (!empty($this->mpdf->title)) { $m .= ' - ' . $this->mpdf->title . ' + ' . htmlspecialchars($this->mpdf->title, ENT_QUOTES | ENT_XML1) . ' ' . "\n"; } if (!empty($this->mpdf->keywords)) { $m .= ' - ' . $this->mpdf->keywords . ' + ' . htmlspecialchars($this->mpdf->keywords, ENT_QUOTES | ENT_XML1) . ' ' . "\n"; } if (!empty($this->mpdf->subject)) { $m .= ' - ' . $this->mpdf->subject . ' + ' . htmlspecialchars($this->mpdf->subject, ENT_QUOTES | ENT_XML1) . ' ' . "\n"; } if (!empty($this->mpdf->author)) { $m .= ' - ' . $this->mpdf->author . ' + ' . htmlspecialchars($this->mpdf->author, ENT_QUOTES | ENT_XML1) . ' ' . "\n"; } @@ -150,7 +148,7 @@ public function writeMetadata() // _putmetadata public function writeInfo() // _putinfo { - $this->writer->write('/Producer ' . $this->writer->utf16BigEndianTextString('mPDF' . ($this->mpdf->exposeVersion ? (' ' . $this->getVersionString()) : ''))); + $this->writer->write('/Producer ' . $this->writer->utf16BigEndianTextString($this->getProducerString())); if (!empty($this->mpdf->title)) { $this->writer->write('/Title ' . $this->writer->utf16BigEndianTextString($this->mpdf->title)); @@ -302,7 +300,7 @@ public function writeAssociatedFiles() // _putAssociatedFiles if ($file['mime']) { $this->writer->write('/Subtype /' . $this->writer->escapeSlashes($file['mime'])); } - $this->writer->write('/Length '.strlen($filestream)); + $this->writer->write('/Length ' . strlen($filestream)); $this->writer->write('/Filter /FlateDecode'); if (isset($file['path'])) { $this->writer->write('/Params <writer->string('D:' . PdfDate::format(filemtime($file['path']))).' >>'); @@ -332,6 +330,12 @@ public function writeCatalog() //_putcatalog $this->writer->write('/Type /Catalog'); $this->writer->write('/Pages 1 0 R'); + if (is_string($this->mpdf->currentLang)) { + $this->writer->write(sprintf('/Lang (%s)', $this->mpdf->currentLang)); + } elseif (is_string($this->mpdf->default_lang)) { + $this->writer->write(sprintf('/Lang (%s)', $this->mpdf->default_lang)); + } + if ($this->mpdf->ZoomMode === 'fullpage') { $this->writer->write('/OpenAction [3 0 R /Fit]'); } elseif ($this->mpdf->ZoomMode === 'fullwidth') { @@ -802,11 +806,6 @@ public function writeTrailer() // _puttrailer } } - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - private function getVersionString() { $return = Mpdf::VERSION; @@ -826,4 +825,9 @@ private function getVersionString() return $return; } + private function getProducerString() + { + return 'mPDF' . ($this->mpdf->exposeVersion ? (' ' . $this->getVersionString()) : ''); + } + } diff --git a/vendor/mpdf/mpdf/src/Writer/ResourceWriter.php b/vendor/mpdf/mpdf/src/Writer/ResourceWriter.php index 996b8823..001d8c8b 100644 --- a/vendor/mpdf/mpdf/src/Writer/ResourceWriter.php +++ b/vendor/mpdf/mpdf/src/Writer/ResourceWriter.php @@ -4,12 +4,14 @@ use Mpdf\Strict; use Mpdf\Mpdf; +use Mpdf\PsrLogAwareTrait\PsrLogAwareTrait; use Psr\Log\LoggerInterface; final class ResourceWriter implements \Psr\Log\LoggerAwareInterface { use Strict; + use PsrLogAwareTrait; /** * @var \Mpdf\Mpdf @@ -66,11 +68,6 @@ final class ResourceWriter implements \Psr\Log\LoggerAwareInterface */ private $javaScriptWriter; - /** - * @var \Psr\Log\LoggerInterface - */ - private $logger; - public function __construct( Mpdf $mpdf, BaseWriter $writer, @@ -243,14 +240,4 @@ public function writeResources() // _putresources $this->writer->write('endobj'); } } - - /** - * @param \Psr\Log\LoggerInterface $logger - * - * @return void - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } } diff --git a/vendor/mpdf/mpdf/src/functions.php b/vendor/mpdf/mpdf/src/functions.php new file mode 100644 index 00000000..bc8b90d9 --- /dev/null +++ b/vendor/mpdf/mpdf/src/functions.php @@ -0,0 +1,25 @@ + array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol; + + /** @var StreamInterface */ + private $stream; + + /** + * @param string $method HTTP method + * @param string|UriInterface $uri URI + * @param array $headers Request headers + * @param string|null|resource|StreamInterface $body Request body + * @param string $version Protocol version + */ + public function __construct( + $method, + $uri, + array $headers = [], + $body = null, + $version = '1.1' + ) { + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = $method; + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!$this->hasHeader('Host')) { + $this->updateHostFromUri(); + } + + if ($body !== '' && $body !== null) { + $this->stream = Stream::create($body); + } + } + + public function getRequestTarget(): string + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target == '') { + $target = '/'; + } + if ($this->uri->getQuery() != '') { + $target .= '?'.$this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget(string $requestTarget): RequestInterface + { + if (preg_match('#\s#', $requestTarget)) { + throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + + return $new; + } + + public function getMethod(): string + { + return $this->method; + } + + public function withMethod(string $method): RequestInterface + { + $new = clone $this; + $new->method = $method; + + return $new; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !$this->hasHeader('Host')) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri() + { + $host = $this->uri->getHost(); + + if ($host == '') { + return; + } + + if (($port = $this->uri->getPort()) !== null) { + $host .= ':'.$port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $header = 'Host'; + $this->headerNames['host'] = 'Host'; + } + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion(string $version): MessageInterface + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader(string $header): bool + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader(string $header): array + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine(string $header):string + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader(string $header, $value): MessageInterface + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader(string $header, $value): MessageInterface + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader(string $header): MessageInterface + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody(): StreamInterface + { + if (!$this->stream) { + $this->stream = Stream::create(''); + $this->stream->rewind(); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body): MessageInterface + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + private function setHeaders(array $headers) + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param string[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function trimHeaderValues(array $values) + { + return array_map(function ($value) { + return trim($value, " \t"); + }, $values); + } + +} diff --git a/vendor/mpdf/psr-http-message-shim/src/Response.php b/vendor/mpdf/psr-http-message-shim/src/Response.php new file mode 100644 index 00000000..34f244f8 --- /dev/null +++ b/vendor/mpdf/psr-http-message-shim/src/Response.php @@ -0,0 +1,263 @@ + 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase; + + /** @var int */ + private $statusCode; + + /** @var array Map of all registered headers, as original name => array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol; + + /** @var \Psr\Http\Message\StreamInterface */ + private $stream; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|resource|StreamInterface|null $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct($status = 200, array $headers = [], $body = null, $version = '1.1', $reason = null) + { + // If we got no body, defer initialization of the stream until Response::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + + $this->statusCode = $status; + $this->setHeaders($headers); + if (null === $reason && isset(self::$phrases[$this->statusCode])) { + $this->reasonPhrase = self::$phrases[$status]; + } else { + $this->reasonPhrase = isset($reason) ? $reason : ''; + } + + $this->protocol = $version; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface + { + if (!\is_int($code) && !\is_string($code)) { + throw new \InvalidArgumentException('Status code has to be an integer'); + } + + $code = (int) $code; + if ($code < 100 || $code > 599) { + throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + } + + $new = clone $this; + $new->statusCode = $code; + if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::$phrases[$new->statusCode])) { + $reasonPhrase = self::$phrases[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + + return $new; + } + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion(string $version): MessageInterface + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader(string $header): bool + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader(string $header): array + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine(string $header): string + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader(string $header, $value): MessageInterface + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader(string $header, $value): MessageInterface + { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader(string $header): MessageInterface + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody(): StreamInterface + { + if (!$this->stream) { + $this->stream = Stream::create(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body): MessageInterface + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + private function setHeaders(array $headers) + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + if (!is_array($value)) { + $value = [$value]; + } + + $value = $this->trimHeaderValues($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param string[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function trimHeaderValues(array $values) + { + return array_map(function ($value) { + return trim($value, " \t"); + }, $values); + } + +} diff --git a/vendor/mpdf/psr-http-message-shim/src/Stream.php b/vendor/mpdf/psr-http-message-shim/src/Stream.php new file mode 100644 index 00000000..f39c2e7e --- /dev/null +++ b/vendor/mpdf/psr-http-message-shim/src/Stream.php @@ -0,0 +1,271 @@ + [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, + ], + ]; + + private function __construct() + { + } + + /** + * @param resource $resource + * + * @return Stream + */ + public static function createFromResource($resource) + { + if (!is_resource($resource)) { + throw new \InvalidArgumentException('Stream must be a resource'); + } + + $obj = new self(); + $obj->stream = $resource; + $meta = stream_get_meta_data($obj->stream); + $obj->seekable = $meta['seekable']; + $obj->readable = isset(self::$readWriteHash['read'][$meta['mode']]); + $obj->writable = isset(self::$readWriteHash['write'][$meta['mode']]); + $obj->uri = $obj->getMetadata('uri'); + + return $obj; + } + + /** + * @param string $content + * + * @return Stream + */ + public static function create($content) + { + $resource = fopen('php://temp', 'rwb+'); + $stream = self::createFromResource($resource); + $stream->write($content); + $stream->seek(0); + + return $stream; + } + + /** + * Closes the stream when the destructed. + */ + public function __destruct() + { + $this->close(); + } + + public function __toString(): string + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + public function close(): void + { + if (isset($this->stream)) { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function getSize(): ?int + { + if ($this->size !== null) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + + return $this->size; + } + return null; + } + + public function tell(): int + { + $result = ftell($this->stream); + + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function eof(): bool + { + return !$this->stream || feof($this->stream); + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + + if (fseek($this->stream, $offset, $whence) === -1) { + throw new \RuntimeException('Unable to seek to stream position '.$offset.' with whence '.var_export($whence, true)); + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return $this->writable; + } + + public function write(string $string): int + { + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + $result = fwrite($this->stream, $string); + + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + public function isReadable(): bool + { + return $this->readable; + } + + public function read(int $length): string + { + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + + return fread($this->stream, $length); + } + + public function getContents(): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Unable to read stream contents'); + } + + $contents = stream_get_contents($this->stream); + + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + + return $contents; + } + + public function getMetadata(?string $key = null): bool + { + if (!isset($this->stream)) { + return $key ? null : []; + } + + if ($key === null) { + return stream_get_meta_data($this->stream); + } + + $meta = stream_get_meta_data($this->stream); + + return isset($meta[$key]) ? $meta[$key] : null; + } + +} diff --git a/vendor/mpdf/psr-http-message-shim/src/Uri.php b/vendor/mpdf/psr-http-message-shim/src/Uri.php new file mode 100644 index 00000000..d7f4074f --- /dev/null +++ b/vendor/mpdf/psr-http-message-shim/src/Uri.php @@ -0,0 +1,305 @@ + 80, 'https' => 443]; + + const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; + + const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + public function __construct($uri = '') + { + if ('' !== $uri) { + if (false === $parts = \parse_url($uri)) { + throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); + } + + // Apply parse_url parts to a URI. + $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; + $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; + $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; + $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; + $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $parts['pass']; + } + } + } + + public function __toString(): string + { + return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getAuthority(): string + { + if ('' === $this->host) { + return ''; + } + + $authority = $this->host; + if ('' !== $this->userInfo) { + $authority = $this->userInfo . '@' . $authority; + } + + if (null !== $this->port) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo(): string + { + return $this->userInfo; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function getPath(): string + { + return $this->path; + } + + public function getQuery(): string + { + return $this->query; + } + + public function getFragment(): string + { + return $this->fragment; + } + + public function withScheme(string $scheme): UriInterface + { + if (!\is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->port = $new->filterPort($new->port); + + return $new; + } + + public function withUserInfo(string $user, ?string $password = null): UriInterface + { + $info = $user; + if (null !== $password && '' !== $password) { + $info .= ':' . $password; + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + + return $new; + } + + public function withHost(string $host): UriInterface + { + if (!\is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->host = $host; + + return $new; + } + + public function withPort(?int $port): UriInterface + { + if ($this->port === $port = $this->filterPort($port)) { + return $this; + } + + $new = clone $this; + $new->port = $port; + + return $new; + } + + public function withPath(string $path): UriInterface + { + if ($this->path === $path = $this->filterPath($path)) { + return $this; + } + + $new = clone $this; + $new->path = $path; + + return $new; + } + + public function withQuery(string $query): UriInterface + { + if ($this->query === $query = $this->filterQueryAndFragment($query)) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment(string $fragment): UriInterface + { + if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Create a URI string from its various parts. + */ + private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string + { + $uri = ''; + if ('' !== $scheme) { + $uri .= $scheme . ':'; + } + + if ('' !== $authority) { + $uri .= '//' . $authority; + } + + if ('' !== $path) { + if ('/' !== $path[0]) { + if ('' !== $authority) { + // If the path is rootless and an authority is present, the path MUST be prefixed by "/" + $path = '/' . $path; + } + } elseif (isset($path[1]) && '/' === $path[1]) { + if ('' === $authority) { + // If the path is starting with more than one "/" and no authority is present, the + // starting slashes MUST be reduced to one. + $path = '/' . \ltrim($path, '/'); + } + } + + $uri .= $path; + } + + if ('' !== $query) { + $uri .= '?' . $query; + } + + if ('' !== $fragment) { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Is a given port non-standard for the current scheme? + */ + private static function isNonStandardPort(string $scheme, int $port): bool + { + return !isset(self::$schemes[$scheme]) || $port !== self::$schemes[$scheme]; + } + + private function filterPort(int $port): ?int + { + if (null === $port) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xffff < $port) { + throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); + } + + return self::isNonStandardPort($this->scheme, $port) ? $port : null; + } + + private function filterPath($path) + { + if (!\is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); + } + + private function filterQueryAndFragment($str) + { + if (!\is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); + } + + private static function rawurlencodeMatchZero(array $match) + { + return \rawurlencode($match[0]); + } + +} diff --git a/vendor/mpdf/psr-log-aware-trait/.gitignore b/vendor/mpdf/psr-log-aware-trait/.gitignore new file mode 100644 index 00000000..d8a7996a --- /dev/null +++ b/vendor/mpdf/psr-log-aware-trait/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/vendor/mpdf/psr-log-aware-trait/README.md b/vendor/mpdf/psr-log-aware-trait/README.md new file mode 100644 index 00000000..9545e772 --- /dev/null +++ b/vendor/mpdf/psr-log-aware-trait/README.md @@ -0,0 +1,20 @@ +# psr-log-aware-trait + +Trait to allow support of different psr/log versions. + +By including this PsrLogAwareTrait, you can allow composer to resolve your PsrLogger version for you. + +## Use + +Require the trait. + + composer require chromatic/psr-log-aware-trait + + +In your code, you no longer have to set a $logger property on your classes, since that comes with the trait, and you do not need to implement the `function setLogger()` method, since that also comes along with the trait. + +```php +use PsrLogAwareTrait; +``` + +Will allow you to call `setLogger()` in your classes and fulfil the requirements of the PsrLoggerAwareInterface implementation. diff --git a/vendor/mpdf/psr-log-aware-trait/composer.json b/vendor/mpdf/psr-log-aware-trait/composer.json new file mode 100644 index 00000000..daf1c388 --- /dev/null +++ b/vendor/mpdf/psr-log-aware-trait/composer.json @@ -0,0 +1,24 @@ +{ + "name": "mpdf/psr-log-aware-trait", + "description": "Trait to allow support of different psr/log versions.", + "type": "library", + "require": { + "psr/log": "^1.0 || ^2.0" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ] +} diff --git a/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php b/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php new file mode 100644 index 00000000..4900229e --- /dev/null +++ b/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php @@ -0,0 +1,27 @@ +logger = $logger; + if (property_exists($this, 'services') && is_array($this->services)) { + foreach ($this->services as $name) { + if ($this->$name && $this->$name instanceof \Psr\Log\LoggerAwareInterface) { + $this->$name->setLogger($logger); + } + } + } + } + +} diff --git a/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php b/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php new file mode 100644 index 00000000..e2db69c3 --- /dev/null +++ b/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php @@ -0,0 +1,20 @@ +logger = $logger; + } + +} diff --git a/vendor/mpdf/qrcode/README.md b/vendor/mpdf/qrcode/README.md index c9be03ce..e819c042 100644 --- a/vendor/mpdf/qrcode/README.md +++ b/vendor/mpdf/qrcode/README.md @@ -22,9 +22,17 @@ use Mpdf\QrCode\Output; $qrCode = new QrCode('Lorem ipsum sit dolor'); +// Save black on white PNG image 100 px wide to filename.png. Colors are RGB arrays. $output = new Output\Png(); - -// Save black on white PNG image 100px wide to filename.png $data = $output->output($qrCode, 100, [255, 255, 255], [0, 0, 0]); file_put_contents('filename.png', $data); + +// Echo a SVG file, 100 px wide, black on white. +// Colors can be specified in SVG-compatible formats +$output = new Output\Svg(); +echo $output->output($qrCode, 100, 'white', 'black'); + +// Echo an HTML table +$output = new Output\Html(); +echo $output->output($qrCode); ``` diff --git a/vendor/mpdf/qrcode/src/QrCode.php b/vendor/mpdf/qrcode/src/QrCode.php index 7c2e2046..4b550bc3 100644 --- a/vendor/mpdf/qrcode/src/QrCode.php +++ b/vendor/mpdf/qrcode/src/QrCode.php @@ -440,13 +440,14 @@ private function encode() $flag = true; while ($flag) { + $this->wordData[$wordCount] = isset($this->wordData[$wordCount]) ? $this->wordData[$wordCount] : 0; if ($remainingBit > $bufferBit) { - $this->wordData[$wordCount] = ((@$this->wordData[$wordCount] << $bufferBit) | $bufferVal); + $this->wordData[$wordCount] = (($this->wordData[$wordCount] << $bufferBit) | $bufferVal); $remainingBit -= $bufferBit; $flag = false; } else { $bufferBit -= $remainingBit; - $this->wordData[$wordCount] = ((@$this->wordData[$wordCount] << $remainingBit) | ($bufferVal >> $bufferBit)); + $this->wordData[$wordCount] = (($this->wordData[$wordCount] << $remainingBit) | ($bufferVal >> $bufferBit)); $wordCount++; if ($bufferBit === 0) { diff --git a/vendor/myclabs/deep-copy/.github/FUNDING.yml b/vendor/myclabs/deep-copy/.github/FUNDING.yml deleted file mode 100644 index b8da664d..00000000 --- a/vendor/myclabs/deep-copy/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: "packagist/myclabs/deep-copy" -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/vendor/myclabs/deep-copy/README.md b/vendor/myclabs/deep-copy/README.md index 007ad5bb..88ae14cc 100644 --- a/vendor/myclabs/deep-copy/README.md +++ b/vendor/myclabs/deep-copy/README.md @@ -2,17 +2,15 @@ DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph. -[![Build Status](https://travis-ci.org/myclabs/DeepCopy.png?branch=1.x)](https://travis-ci.org/myclabs/DeepCopy) -[![Coverage Status](https://coveralls.io/repos/myclabs/DeepCopy/badge.png?branch=1.x)](https://coveralls.io/r/myclabs/DeepCopy?branch=1.x) -[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/myclabs/DeepCopy/badges/quality-score.png?s=2747100c19b275f93a777e3297c6c12d1b68b934)](https://scrutinizer-ci.com/g/myclabs/DeepCopy/) [![Total Downloads](https://poser.pugx.org/myclabs/deep-copy/downloads.svg)](https://packagist.org/packages/myclabs/deep-copy) +[![Integrate](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml/badge.svg?branch=1.x)](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml) ## Table of Contents 1. [How](#how) 1. [Why](#why) 1. [Using simply `clone`](#using-simply-clone) - 1. [Overridding `__clone()`](#overridding-__clone) + 1. [Overriding `__clone()`](#overriding-__clone) 1. [With `DeepCopy`](#with-deepcopy) 1. [How it works](#how-it-works) 1. [Going further](#going-further) @@ -37,11 +35,11 @@ DeepCopy helps you create deep copies (clones) of your objects. It is designed t Install with Composer: -```json +``` composer require myclabs/deep-copy ``` -Use simply: +Use it: ```php use DeepCopy\DeepCopy; @@ -76,9 +74,9 @@ Now you're in for a big mess :( ![Using clone](doc/clone.png) -### Overridding `__clone()` +### Overriding `__clone()` -![Overridding __clone](doc/deep-clone.png) +![Overriding __clone](doc/deep-clone.png) ### With `DeepCopy` @@ -188,6 +186,9 @@ $matcher = new TypeMatcher('Doctrine\Common\Collections\Collection'); - `DeepCopy\Filter` applies a transformation to the object attribute matched by `DeepCopy\Matcher` - `DeepCopy\TypeFilter` applies a transformation to any element matched by `DeepCopy\TypeMatcher` +By design, matching a filter will stop the chain of filters (i.e. the next ones will not be applied). +Using the ([`ChainableFilter`](#chainablefilter-filter)) won't stop the chain of filters. + #### `SetNullFilter` (filter) @@ -228,6 +229,34 @@ $copy = $copier->copy($object); ``` +#### `ChainableFilter` (filter) + +If you use cloning on proxy classes, you might want to apply two filters for: +1. loading the data +2. applying a transformation + +You can use the `ChainableFilter` as a decorator of the proxy loader filter, which won't stop the chain of filters (i.e. +the next ones may be applied). + + +```php +use DeepCopy\DeepCopy; +use DeepCopy\Filter\ChainableFilter; +use DeepCopy\Filter\Doctrine\DoctrineProxyFilter; +use DeepCopy\Filter\SetNullFilter; +use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher; +use DeepCopy\Matcher\PropertyNameMatcher; + +$copier = new DeepCopy(); +$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher()); +$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id')); + +$copy = $copier->copy($object); + +echo $copy->id; // null +``` + + #### `DoctrineCollectionFilter` (filter) If you use Doctrine and want to copy an entity, you will need to use the `DoctrineCollectionFilter`: @@ -270,6 +299,8 @@ Doctrine proxy class (...\\\_\_CG\_\_\Proxy). You can use the `DoctrineProxyFilter` to load the actual entity behind the Doctrine proxy class. **Make sure, though, to put this as one of your very first filters in the filter chain so that the entity is loaded before other filters are applied!** +We recommend to decorate the `DoctrineProxyFilter` with the `ChainableFilter` to allow applying other filters to the +cloned lazy loaded entities. ```php use DeepCopy\DeepCopy; @@ -277,7 +308,7 @@ use DeepCopy\Filter\Doctrine\DoctrineProxyFilter; use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher; $copier = new DeepCopy(); -$copier->addFilter(new DoctrineProxyFilter(), new DoctrineProxyMatcher()); +$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher()); $copy = $copier->copy($object); diff --git a/vendor/myclabs/deep-copy/composer.json b/vendor/myclabs/deep-copy/composer.json index 45656c91..f115fff8 100644 --- a/vendor/myclabs/deep-copy/composer.json +++ b/vendor/myclabs/deep-copy/composer.json @@ -1,10 +1,28 @@ { "name": "myclabs/deep-copy", - "type": "library", "description": "Create deep copies (clones) of your objects", - "keywords": ["clone", "copy", "duplicate", "object", "object graph"], "license": "MIT", - + "type": "library", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" @@ -15,23 +33,10 @@ }, "autoload-dev": { "psr-4": { - "DeepCopy\\": "fixtures/", - "DeepCopyTest\\": "tests/DeepCopyTest/" + "DeepCopyTest\\": "tests/DeepCopyTest/", + "DeepCopy\\": "fixtures/" } }, - - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "config": { "sort-packages": true } diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php b/vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php index 15e5c689..a944697d 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php @@ -4,13 +4,16 @@ use ArrayObject; use DateInterval; +use DatePeriod; use DateTimeInterface; use DateTimeZone; use DeepCopy\Exception\CloneException; +use DeepCopy\Filter\ChainableFilter; use DeepCopy\Filter\Filter; use DeepCopy\Matcher\Matcher; use DeepCopy\Reflection\ReflectionHelper; use DeepCopy\TypeFilter\Date\DateIntervalFilter; +use DeepCopy\TypeFilter\Date\DatePeriodFilter; use DeepCopy\TypeFilter\Spl\ArrayObjectFilter; use DeepCopy\TypeFilter\Spl\SplDoublyLinkedListFilter; use DeepCopy\TypeFilter\TypeFilter; @@ -63,6 +66,7 @@ public function __construct($useCloneMethod = false) $this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class)); $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class)); + $this->addTypeFilter(new DatePeriodFilter(), new TypeMatcher(DatePeriod::class)); $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class)); } @@ -83,9 +87,11 @@ public function skipUncloneable($skipUncloneable = true) /** * Deep copies the given object. * - * @param mixed $object + * @template TObject * - * @return mixed + * @param TObject $object + * + * @return TObject */ public function copy($object) { @@ -118,6 +124,14 @@ public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher) ]; } + public function prependTypeFilter(TypeFilter $filter, TypeMatcher $matcher) + { + array_unshift($this->typeFilters, [ + 'matcher' => $matcher, + 'filter' => $filter, + ]); + } + private function recursiveCopy($var) { // Matches Type Filter @@ -140,6 +154,11 @@ private function recursiveCopy($var) return $var; } + // Enum + if (PHP_VERSION_ID >= 80100 && enum_exists(get_class($var))) { + return $var; + } + // Object return $this->copyObject($var); } @@ -218,6 +237,11 @@ private function copyObjectProperty($object, ReflectionProperty $property) return; } + // Ignore readonly properties + if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) { + return; + } + // Apply the filters foreach ($this->filters as $item) { /** @var Matcher $matcher */ @@ -234,12 +258,18 @@ function ($object) { } ); + if ($filter instanceof ChainableFilter) { + continue; + } + // If a filter matches, we stop processing this property return; } } - $property->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } // Ignore uninitialized properties (for PHP >7.4) if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) { diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ChainableFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ChainableFilter.php new file mode 100644 index 00000000..4e3f7bbc --- /dev/null +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ChainableFilter.php @@ -0,0 +1,24 @@ +filter = $filter; + } + + public function apply($object, $property, $objectCopier) + { + $this->filter->apply($object, $property, $objectCopier); + } +} diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php index e6d93771..66e91e59 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php @@ -19,7 +19,9 @@ public function apply($object, $property, $objectCopier) { $reflectionProperty = ReflectionHelper::getProperty($object, $property); - $reflectionProperty->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } $oldCollection = $reflectionProperty->getValue($object); $newCollection = $oldCollection->map( diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineEmptyCollectionFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineEmptyCollectionFilter.php index 7b33fd54..fa1c0341 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineEmptyCollectionFilter.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/Doctrine/DoctrineEmptyCollectionFilter.php @@ -21,7 +21,9 @@ class DoctrineEmptyCollectionFilter implements Filter public function apply($object, $property, $objectCopier) { $reflectionProperty = ReflectionHelper::getProperty($object, $property); - $reflectionProperty->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } $reflectionProperty->setValue($object, new ArrayCollection()); } diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ReplaceFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ReplaceFilter.php index 7aca593b..fda8e726 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ReplaceFilter.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/ReplaceFilter.php @@ -30,7 +30,9 @@ public function __construct(callable $callable) public function apply($object, $property, $objectCopier) { $reflectionProperty = ReflectionHelper::getProperty($object, $property); - $reflectionProperty->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } $value = call_user_func($this->callback, $reflectionProperty->getValue($object)); diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/SetNullFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/SetNullFilter.php index bea86b88..67222724 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Filter/SetNullFilter.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Filter/SetNullFilter.php @@ -18,7 +18,9 @@ public function apply($object, $property, $objectCopier) { $reflectionProperty = ReflectionHelper::getProperty($object, $property); - $reflectionProperty->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } $reflectionProperty->setValue($object, null); } } diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php b/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php index ec8856f5..c5887b19 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php @@ -3,7 +3,7 @@ namespace DeepCopy\Matcher\Doctrine; use DeepCopy\Matcher\Matcher; -use Doctrine\Common\Persistence\Proxy; +use Doctrine\Persistence\Proxy; /** * @final diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyTypeMatcher.php b/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyTypeMatcher.php index a6b0c0bc..7980bfa2 100644 --- a/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyTypeMatcher.php +++ b/vendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyTypeMatcher.php @@ -39,7 +39,15 @@ public function matches($object, $property) return false; } - $reflectionProperty->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } + + // Uninitialized properties (for PHP >7.4) + if (method_exists($reflectionProperty, 'isInitialized') && !$reflectionProperty->isInitialized($object)) { + // null instanceof $this->propertyType + return false; + } return $reflectionProperty->getValue($object) instanceof $this->propertyType; } diff --git a/vendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Date/DatePeriodFilter.php b/vendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Date/DatePeriodFilter.php new file mode 100644 index 00000000..6bd2f7e5 --- /dev/null +++ b/vendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Date/DatePeriodFilter.php @@ -0,0 +1,42 @@ += 80200 && $element->include_end_date) { + $options |= DatePeriod::INCLUDE_END_DATE; + } + if (!$element->include_start_date) { + $options |= DatePeriod::EXCLUDE_START_DATE; + } + + if ($element->getEndDate()) { + return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $element->getEndDate(), $options); + } + + if (PHP_VERSION_ID >= 70217) { + $recurrences = $element->getRecurrences(); + } else { + $recurrences = $element->recurrences - $element->include_start_date; + } + + return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $recurrences, $options); + } +} diff --git a/vendor/psr/http-message/CHANGELOG.md b/vendor/psr/http-message/CHANGELOG.md new file mode 100644 index 00000000..74b1ef92 --- /dev/null +++ b/vendor/psr/http-message/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.1 - 2016-08-06 + +### Added + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Updated all `@return self` annotation references in interfaces to use + `@return static`, which more closelly follows the semantics of the + specification. +- Updated the `MessageInterface::getHeaders()` return annotation to use the + value `string[][]`, indicating the format is a nested array of strings. +- Updated the `@link` annotation for `RequestInterface::withRequestTarget()` + to point to the correct section of RFC 7230. +- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation + to add the parameter name (`$uploadedFiles`). +- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()` + method to correctly reference the method parameter (it was referencing an + incorrect parameter name previously). + +## 1.0.0 - 2016-05-18 + +Initial stable release; reflects accepted PSR-7 specification. diff --git a/vendor/psr/http-message/LICENSE b/vendor/psr/http-message/LICENSE new file mode 100644 index 00000000..c2d8e452 --- /dev/null +++ b/vendor/psr/http-message/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/psr/http-message/README.md b/vendor/psr/http-message/README.md new file mode 100644 index 00000000..2668be6c --- /dev/null +++ b/vendor/psr/http-message/README.md @@ -0,0 +1,16 @@ +PSR Http Message +================ + +This repository holds all interfaces/classes/traits related to +[PSR-7](http://www.php-fig.org/psr/psr-7/). + +Note that this is not a HTTP message implementation of its own. It is merely an +interface that describes a HTTP message. See the specification for more details. + +Usage +----- + +Before reading the usage guide we recommend reading the PSR-7 interfaces method list: + +* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md) +* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md) \ No newline at end of file diff --git a/vendor/psr/http-message/composer.json b/vendor/psr/http-message/composer.json new file mode 100644 index 00000000..c66e5aba --- /dev/null +++ b/vendor/psr/http-message/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/http-message", + "description": "Common interface for HTTP messages", + "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"], + "homepage": "https://github.com/php-fig/http-message", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": "^7.2 || ^8.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + } +} diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 00000000..a83c9851 --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(): array; + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader(string $name): bool; + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader(string $name): array; + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine(string $name): string; + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader(string $name, $value): MessageInterface; + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader(string $name, $value): MessageInterface; + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name): MessageInterface; + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(): StreamInterface; + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body): MessageInterface; +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 00000000..33f85e55 --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,130 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(): array; + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface; + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(): array; + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data): ServerRequestInterface; + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(): array; + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute(string $name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, $value): ServerRequestInterface; + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name): ServerRequestInterface; +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 00000000..a62aabb8 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(): string; + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(): string; + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(): string; + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(): ?int; + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(): string; + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(): string; + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(): string; + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme): UriInterface; + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo(string $user, ?string $password = null): UriInterface; + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost(string $host): UriInterface; + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort(?int $port): UriInterface; + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath(string $path): UriInterface; + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery(string $query): UriInterface; + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment(string $fragment): UriInterface; + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(): string; +} diff --git a/vendor/psr/log/Psr/Log/AbstractLogger.php b/vendor/psr/log/Psr/Log/AbstractLogger.php deleted file mode 100644 index 90e721af..00000000 --- a/vendor/psr/log/Psr/Log/AbstractLogger.php +++ /dev/null @@ -1,128 +0,0 @@ -log(LogLevel::EMERGENCY, $message, $context); - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function alert($message, array $context = array()) - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function critical($message, array $context = array()) - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function error($message, array $context = array()) - { - $this->log(LogLevel::ERROR, $message, $context); - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function warning($message, array $context = array()) - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function notice($message, array $context = array()) - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function info($message, array $context = array()) - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function debug($message, array $context = array()) - { - $this->log(LogLevel::DEBUG, $message, $context); - } -} diff --git a/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/vendor/psr/log/Psr/Log/InvalidArgumentException.php deleted file mode 100644 index 67f852d1..00000000 --- a/vendor/psr/log/Psr/Log/InvalidArgumentException.php +++ /dev/null @@ -1,7 +0,0 @@ -logger = $logger; - } -} diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/Psr/Log/LoggerInterface.php deleted file mode 100644 index 2206cfde..00000000 --- a/vendor/psr/log/Psr/Log/LoggerInterface.php +++ /dev/null @@ -1,125 +0,0 @@ -log(LogLevel::EMERGENCY, $message, $context); - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function alert($message, array $context = array()) - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function critical($message, array $context = array()) - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function error($message, array $context = array()) - { - $this->log(LogLevel::ERROR, $message, $context); - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function warning($message, array $context = array()) - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function notice($message, array $context = array()) - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function info($message, array $context = array()) - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function debug($message, array $context = array()) - { - $this->log(LogLevel::DEBUG, $message, $context); - } - - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void - * - * @throws \Psr\Log\InvalidArgumentException - */ - abstract public function log($level, $message, array $context = array()); -} diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/Psr/Log/NullLogger.php deleted file mode 100644 index c8f7293b..00000000 --- a/vendor/psr/log/Psr/Log/NullLogger.php +++ /dev/null @@ -1,30 +0,0 @@ -logger) { }` - * blocks. - */ -class NullLogger extends AbstractLogger -{ - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void - * - * @throws \Psr\Log\InvalidArgumentException - */ - public function log($level, $message, array $context = array()) - { - // noop - } -} diff --git a/vendor/psr/log/Psr/Log/Test/DummyTest.php b/vendor/psr/log/Psr/Log/Test/DummyTest.php deleted file mode 100644 index 9638c110..00000000 --- a/vendor/psr/log/Psr/Log/Test/DummyTest.php +++ /dev/null @@ -1,18 +0,0 @@ - ". - * - * Example ->error('Foo') would yield "error Foo". - * - * @return string[] - */ - abstract public function getLogs(); - - public function testImplements() - { - $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); - } - - /** - * @dataProvider provideLevelsAndMessages - */ - public function testLogsAtAllLevels($level, $message) - { - $logger = $this->getLogger(); - $logger->{$level}($message, array('user' => 'Bob')); - $logger->log($level, $message, array('user' => 'Bob')); - - $expected = array( - $level.' message of level '.$level.' with context: Bob', - $level.' message of level '.$level.' with context: Bob', - ); - $this->assertEquals($expected, $this->getLogs()); - } - - public function provideLevelsAndMessages() - { - return array( - LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), - LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), - LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), - LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), - LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), - LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), - LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), - LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), - ); - } - - /** - * @expectedException \Psr\Log\InvalidArgumentException - */ - public function testThrowsOnInvalidLevel() - { - $logger = $this->getLogger(); - $logger->log('invalid level', 'Foo'); - } - - public function testContextReplacement() - { - $logger = $this->getLogger(); - $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); - - $expected = array('info {Message {nothing} Bob Bar a}'); - $this->assertEquals($expected, $this->getLogs()); - } - - public function testObjectCastToString() - { - if (method_exists($this, 'createPartialMock')) { - $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); - } else { - $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); - } - $dummy->expects($this->once()) - ->method('__toString') - ->will($this->returnValue('DUMMY')); - - $this->getLogger()->warning($dummy); - - $expected = array('warning DUMMY'); - $this->assertEquals($expected, $this->getLogs()); - } - - public function testContextCanContainAnything() - { - $closed = fopen('php://memory', 'r'); - fclose($closed); - - $context = array( - 'bool' => true, - 'null' => null, - 'string' => 'Foo', - 'int' => 0, - 'float' => 0.5, - 'nested' => array('with object' => new DummyTest), - 'object' => new \DateTime, - 'resource' => fopen('php://memory', 'r'), - 'closed' => $closed, - ); - - $this->getLogger()->warning('Crazy context data', $context); - - $expected = array('warning Crazy context data'); - $this->assertEquals($expected, $this->getLogs()); - } - - public function testContextExceptionKeyCanBeExceptionOrOtherValues() - { - $logger = $this->getLogger(); - $logger->warning('Random message', array('exception' => 'oops')); - $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); - - $expected = array( - 'warning Random message', - 'critical Uncaught Exception!' - ); - $this->assertEquals($expected, $this->getLogs()); - } -} diff --git a/vendor/psr/log/Psr/Log/Test/TestLogger.php b/vendor/psr/log/Psr/Log/Test/TestLogger.php deleted file mode 100644 index 1be32304..00000000 --- a/vendor/psr/log/Psr/Log/Test/TestLogger.php +++ /dev/null @@ -1,147 +0,0 @@ - $level, - 'message' => $message, - 'context' => $context, - ]; - - $this->recordsByLevel[$record['level']][] = $record; - $this->records[] = $record; - } - - public function hasRecords($level) - { - return isset($this->recordsByLevel[$level]); - } - - public function hasRecord($record, $level) - { - if (is_string($record)) { - $record = ['message' => $record]; - } - return $this->hasRecordThatPasses(function ($rec) use ($record) { - if ($rec['message'] !== $record['message']) { - return false; - } - if (isset($record['context']) && $rec['context'] !== $record['context']) { - return false; - } - return true; - }, $level); - } - - public function hasRecordThatContains($message, $level) - { - return $this->hasRecordThatPasses(function ($rec) use ($message) { - return strpos($rec['message'], $message) !== false; - }, $level); - } - - public function hasRecordThatMatches($regex, $level) - { - return $this->hasRecordThatPasses(function ($rec) use ($regex) { - return preg_match($regex, $rec['message']) > 0; - }, $level); - } - - public function hasRecordThatPasses(callable $predicate, $level) - { - if (!isset($this->recordsByLevel[$level])) { - return false; - } - foreach ($this->recordsByLevel[$level] as $i => $rec) { - if (call_user_func($predicate, $rec, $i)) { - return true; - } - } - return false; - } - - public function __call($method, $args) - { - if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { - $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; - $level = strtolower($matches[2]); - if (method_exists($this, $genericMethod)) { - $args[] = $level; - return call_user_func_array([$this, $genericMethod], $args); - } - } - throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); - } - - public function reset() - { - $this->records = []; - $this->recordsByLevel = []; - } -} diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json index 3f6d4eea..ca056953 100644 --- a/vendor/psr/log/composer.json +++ b/vendor/psr/log/composer.json @@ -7,7 +7,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "require": { diff --git a/vendor/setasign/fpdi/LICENSE.txt b/vendor/setasign/fpdi/LICENSE.txt index 45672fab..84e590c1 100644 --- a/vendor/setasign/fpdi/LICENSE.txt +++ b/vendor/setasign/fpdi/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Setasign GmbH & Co. KG, https://www.setasign.com +Copyright (c) 2024 Setasign GmbH & Co. KG, https://www.setasign.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/setasign/fpdi/README.md b/vendor/setasign/fpdi/README.md index 082122ff..18998fb5 100644 --- a/vendor/setasign/fpdi/README.md +++ b/vendor/setasign/fpdi/README.md @@ -3,9 +3,7 @@ FPDI - Free PDF Document Importer [![Latest Stable Version](https://poser.pugx.org/setasign/fpdi/v/stable.svg)](https://packagist.org/packages/setasign/fpdi) [![Total Downloads](https://poser.pugx.org/setasign/fpdi/downloads.svg)](https://packagist.org/packages/setasign/fpdi) -[![Latest Unstable Version](https://poser.pugx.org/setasign/fpdi/v/unstable.svg)](https://packagist.org/packages/setasign/fpdi) [![License](https://poser.pugx.org/setasign/fpdi/license.svg)](https://packagist.org/packages/setasign/fpdi) -[![Build Status](https://travis-ci.org/Setasign/FPDI.svg?branch=development)](https://travis-ci.org/Setasign/FPDI) :heavy_exclamation_mark: This document refers to FPDI 2. Version 1 is deprecated and development is discontinued. :heavy_exclamation_mark: @@ -28,7 +26,7 @@ To use FPDI with FPDF include following in your composer.json file: { "require": { "setasign/fpdf": "1.8.*", - "setasign/fpdi": "^2.0" + "setasign/fpdi": "^2.5" } } ``` @@ -38,8 +36,8 @@ If you want to use TCPDF, you have to update your composer.json to: ```json { "require": { - "tecnickcom/tcpdf": "6.2.*", - "setasign/fpdi": "^2.0" + "tecnickcom/tcpdf": "6.6.*", + "setasign/fpdi": "^2.5" } } ``` @@ -49,7 +47,7 @@ If you want to use tFPDF, you have to update your composer.json to: ```json { "require": { - "setasign/tfpdf": "1.31.*", + "setasign/tfpdf": "1.33.*", "setasign/fpdi": "^2.3" } } @@ -129,4 +127,4 @@ $pdf->useTemplate($tplId, 10, 10, 100); $pdf->Output(); ``` -A full end-user documentation and API reference is available [here](https://manuals.setasign.com/fpdi-manual/). \ No newline at end of file +A full end-user documentation and API reference is available [here](https://manuals.setasign.com/fpdi-manual/). diff --git a/vendor/setasign/fpdi/composer.json b/vendor/setasign/fpdi/composer.json index 72ea24e0..6f364bf8 100644 --- a/vendor/setasign/fpdi/composer.json +++ b/vendor/setasign/fpdi/composer.json @@ -15,7 +15,7 @@ } }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1 || ^8.0", "ext-zlib": "*" }, "conflict": { @@ -37,10 +37,11 @@ "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." }, "require-dev": { - "phpunit/phpunit": "~5.7", - "setasign/fpdf": "~1.8", - "tecnickcom/tcpdf": "~6.2", - "setasign/tfpdf": "1.31" + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "tecnickcom/tcpdf": "^6.8", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5" }, "autoload-dev": { "psr-4": { diff --git a/vendor/setasign/fpdi/src/FpdfTpl.php b/vendor/setasign/fpdi/src/FpdfTpl.php index d70583d4..e6a15e7d 100644 --- a/vendor/setasign/fpdi/src/FpdfTpl.php +++ b/vendor/setasign/fpdi/src/FpdfTpl.php @@ -1,9 +1,10 @@ _getpagesize($size); - if ($orientation != $this->CurOrientation + if ( + $orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1] ) { @@ -109,7 +109,7 @@ public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, unset($x['tpl']); \extract($x, EXTR_IF_EXISTS); /** @noinspection NotOptimalIfConditionsInspection */ - /** @noinspection CallableParameterUseCaseInTypeContextInspection */ + /** @phpstan-ignore function.alreadyNarrowedType */ if (\is_array($x)) { $x = 0; } @@ -234,7 +234,9 @@ public function beginTemplate($width = null, $height = null, $groupXObject = fal 'lMargin' => $this->lMargin, 'rMargin' => $this->rMargin, 'h' => $this->h, + 'hPt' => $this->hPt, 'w' => $this->w, + 'wPt' => $this->wPt, 'FontFamily' => $this->FontFamily, 'FontStyle' => $this->FontStyle, 'FontSizePt' => $this->FontSizePt, @@ -251,7 +253,9 @@ public function beginTemplate($width = null, $height = null, $groupXObject = fal $this->currentTemplateId = $templateId; $this->h = $height; + $this->hPt = $height * $this->k; $this->w = $width; + $this->wPt = $width * $this->k; $this->SetXY($this->lMargin, $this->tMargin); $this->SetRightMargin($this->w - $width + $this->rMargin); @@ -266,7 +270,7 @@ public function beginTemplate($width = null, $height = null, $groupXObject = fal */ public function endTemplate() { - if (null === $this->currentTemplateId) { + if ($this->currentTemplateId === null) { return false; } @@ -279,7 +283,9 @@ public function endTemplate() $this->lMargin = $state['lMargin']; $this->rMargin = $state['rMargin']; $this->h = $state['h']; + $this->hPt = $state['hPt']; $this->w = $state['w']; + $this->wPt = $state['wPt']; $this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']); $this->FontFamily = $state['FontFamily']; @@ -406,9 +412,6 @@ public function SetFontSize($size) } } - /** - * @inheritdoc - */ protected function _putimages() { parent::_putimages(); @@ -418,7 +421,11 @@ protected function _putimages() $this->templates[$key]['objectNumber'] = $this->n; $this->_put('<_put(\sprintf('/BBox[0 0 %.2F %.2F]', $template['width'] * $this->k, $template['height'] * $this->k)); + $this->_put(\sprintf( + '/BBox[0 0 %.2F %.2F]', + $template['width'] * $this->k, + $template['height'] * $this->k + )); $this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF if ($this->compress) { @@ -463,4 +470,4 @@ public function _out($s) parent::_out($s); } } -} \ No newline at end of file +} diff --git a/vendor/setasign/fpdi/src/FpdfTrait.php b/vendor/setasign/fpdi/src/FpdfTrait.php new file mode 100644 index 00000000..e48ee9d9 --- /dev/null +++ b/vendor/setasign/fpdi/src/FpdfTrait.php @@ -0,0 +1,193 @@ +cleanUp(); + } + + /** + * Draws an imported page or a template onto the page or another template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array + * with the keys "x", "y", "width", "height", "adjustPageSize". + * @param float|int $y The ordinate of upper-left corner. + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @param bool $adjustPageSize + * @return array The size + * @see Fpdi::getTemplateSize() + */ + public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (isset($this->importedPages[$tpl])) { + $size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); + if ($this->currentTemplateId !== null) { + $this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl; + } + return $size; + } + + return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize); + } + + /** + * Get the size of an imported page or template. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $tpl The template id + * @param float|int|null $width The width. + * @param float|int|null $height The height. + * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) + */ + public function getTemplateSize($tpl, $width = null, $height = null) + { + $size = parent::getTemplateSize($tpl, $width, $height); + if ($size === false) { + return $this->getImportedPageSize($tpl, $width, $height); + } + + return $size; + } + + /** + * @throws CrossReferenceException + * @throws PdfParserException + */ + protected function _putimages() + { + $this->currentReaderId = null; + parent::_putimages(); + + foreach ($this->importedPages as $key => $pageData) { + $this->_newobj(); + $this->importedPages[$key]['objectNumber'] = $this->n; + $this->currentReaderId = $pageData['readerId']; + $this->writePdfType($pageData['stream']); + $this->_put('endobj'); + } + + foreach (\array_keys($this->readers) as $readerId) { + $parser = $this->getPdfReader($readerId)->getParser(); + $this->currentReaderId = $readerId; + + while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { + try { + $object = $parser->getIndirectObject($objectNumber); + } catch (CrossReferenceException $e) { + if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { + $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); + } else { + throw $e; + } + } + + $this->writePdfType($object); + } + } + + $this->currentReaderId = null; + } + + /** + * @inheritdoc + */ + protected function _putxobjectdict() + { + foreach ($this->importedPages as $pageData) { + $this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R'); + } + + parent::_putxobjectdict(); + } + + /** + * @param int $n + * @return void + * @throws PdfParser\Type\PdfTypeException + */ + protected function _putlinks($n) + { + foreach ($this->PageLinks[$n] as $pl) { + $this->_newobj(); + $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); + $this->_put('<_put('/A <_escape($pl[4]) . ')>>'); + $values = $pl['importedLink']['pdfObject']->value; + + foreach ($values as $name => $entry) { + $this->_put('/' . $name . ' ', false); + $this->writePdfType($entry); + } + + if (isset($pl['quadPoints'])) { + $s = '/QuadPoints['; + foreach ($pl['quadPoints'] as $value) { + $s .= sprintf('%.2F ', $value); + } + $s .= ']'; + $this->_put($s); + } + } else { + $this->_put('/A <_textstring($pl[4]) . '>>'); + $this->_put('/Border [0 0 0]', false); + } + $this->_put('>>'); + } else { + $this->_put('/Border [0 0 0] ', false); + $l = $this->links[$pl[4]]; + if (isset($this->PageInfo[$l[0]]['size'])) { + $h = $this->PageInfo[$l[0]]['size'][1]; + } else { + $h = ($this->DefOrientation === 'P') + ? $this->DefPageSize[1] * $this->k + : $this->DefPageSize[0] * $this->k; + } + $this->_put(sprintf( + '/Dest [%d 0 R /XYZ 0 %.2F null]>>', + $this->PageInfo[$l[0]]['n'], + $h - $l[1] * $this->k + )); + } + $this->_put('endobj'); + } + } + + protected function _put($s, $newLine = true) + { + if ($newLine) { + $this->buffer .= $s . "\n"; + } else { + $this->buffer .= $s; + } + } +} diff --git a/vendor/setasign/fpdi/src/Fpdi.php b/vendor/setasign/fpdi/src/Fpdi.php index 3db4b55a..147daeea 100644 --- a/vendor/setasign/fpdi/src/Fpdi.php +++ b/vendor/setasign/fpdi/src/Fpdi.php @@ -1,9 +1,10 @@ cleanUp(); - } - - /** - * Draws an imported page or a template onto the page or another template. - * - * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the - * aspect ratio. - * - * @param mixed $tpl The template id - * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array - * with the keys "x", "y", "width", "height", "adjustPageSize". - * @param float|int $y The ordinate of upper-left corner. - * @param float|int|null $width The width. - * @param float|int|null $height The height. - * @param bool $adjustPageSize - * @return array The size - * @see Fpdi::getTemplateSize() - */ - public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) - { - if (isset($this->importedPages[$tpl])) { - $size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); - if ($this->currentTemplateId !== null) { - $this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl; - } - return $size; - } - - return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize); - } - - /** - * Get the size of an imported page or template. - * - * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the - * aspect ratio. - * - * @param mixed $tpl The template id - * @param float|int|null $width The width. - * @param float|int|null $height The height. - * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) - */ - public function getTemplateSize($tpl, $width = null, $height = null) - { - $size = parent::getTemplateSize($tpl, $width, $height); - if ($size === false) { - return $this->getImportedPageSize($tpl, $width, $height); - } - - return $size; - } - - /** - * @inheritdoc - * @throws CrossReferenceException - * @throws PdfParserException - */ - protected function _putimages() - { - $this->currentReaderId = null; - parent::_putimages(); - - foreach ($this->importedPages as $key => $pageData) { - $this->_newobj(); - $this->importedPages[$key]['objectNumber'] = $this->n; - $this->currentReaderId = $pageData['readerId']; - $this->writePdfType($pageData['stream']); - $this->_put('endobj'); - } - - foreach (\array_keys($this->readers) as $readerId) { - $parser = $this->getPdfReader($readerId)->getParser(); - $this->currentReaderId = $readerId; - - while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { - try { - $object = $parser->getIndirectObject($objectNumber); - - } catch (CrossReferenceException $e) { - if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { - $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); - } else { - throw $e; - } - } - - $this->writePdfType($object); - } - } - - $this->currentReaderId = null; - } - - /** - * @inheritdoc - */ - protected function _putxobjectdict() - { - foreach ($this->importedPages as $key => $pageData) { - $this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R'); - } - - parent::_putxobjectdict(); - } - - /** - * @inheritdoc - */ - protected function _put($s, $newLine = true) - { - if ($newLine) { - $this->buffer .= $s . "\n"; - } else { - $this->buffer .= $s; - } - } + const VERSION = '2.6.4'; } diff --git a/vendor/setasign/fpdi/src/FpdiException.php b/vendor/setasign/fpdi/src/FpdiException.php index dcd52c9b..a178c412 100644 --- a/vendor/setasign/fpdi/src/FpdiException.php +++ b/vendor/setasign/fpdi/src/FpdiException.php @@ -1,9 +1,10 @@ readers[$id]); } - $this->createdReaders= []; + $this->createdReaders = []; } /** @@ -124,14 +124,18 @@ protected function setMinPdfVersion($pdfVersion) * Get a new pdf parser instance. * * @param StreamReader $streamReader + * @param array $parserParams Individual parameters passed to the parser instance. * @return PdfParser|FpdiPdfParser */ - protected function getPdfParserInstance(StreamReader $streamReader) + protected function getPdfParserInstance(StreamReader $streamReader, array $parserParams = []) { + // note: if you get an exception here - turn off errors/warnings on not found classes for your autoloader. + // psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw + // exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value. /** @noinspection PhpUndefinedClassInspection */ if (\class_exists(FpdiPdfParser::class)) { /** @noinspection PhpUndefinedClassInspection */ - return new FpdiPdfParser($streamReader); + return new FpdiPdfParser($streamReader, $parserParams); } return new PdfParser($streamReader); @@ -142,9 +146,10 @@ protected function getPdfParserInstance(StreamReader $streamReader) * * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader * instance or a StreamReader instance. + * @param array $parserParams Individual parameters passed to the parser instance. * @return string */ - protected function getPdfReaderId($file) + protected function getPdfReaderId($file, array $parserParams = []) { if (\is_resource($file)) { $id = (string) $file; @@ -175,7 +180,7 @@ protected function getPdfReaderId($file) $streamReader = $file; } - $reader = new PdfReader($this->getPdfParserInstance($streamReader)); + $reader = new PdfReader($this->getPdfParserInstance($streamReader, $parserParams)); /** @noinspection OffsetOperationsInspection */ $this->readers[$id] = $reader; @@ -208,7 +213,24 @@ protected function getPdfReader($id) */ public function setSourceFile($file) { - $this->currentReaderId = $this->getPdfReaderId($file); + return $this->setSourceFileWithParserParams($file); + } + + /** + * Set the source PDF file with parameters which are passed to the parser instance. + * + * This method allows us to pass e.g. authentication information to the parser instance. + * + * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. + * @param array $parserParams Individual parameters passed to the parser instance. + * @return int The page count of the PDF document. + * @throws CrossReferenceException + * @throws PdfParserException + * @throws PdfTypeException + */ + public function setSourceFileWithParserParams($file, array $parserParams = []) + { + $this->currentReaderId = $this->getPdfReaderId($file, $parserParams); $this->objectsToCopy[$this->currentReaderId] = []; $reader = $this->getPdfReader($this->currentReaderId); @@ -223,6 +245,7 @@ public function setSourceFile($file) * @param int $pageNumber The page number. * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX. * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). + * @param bool $importExternalLinks Define whether external links are imported or not. * @return string A unique string identifying the imported page. * @throws CrossReferenceException * @throws FilterException @@ -231,16 +254,20 @@ public function setSourceFile($file) * @throws PdfReaderException * @see PageBoundaries */ - public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true) - { - if (null === $this->currentReaderId) { + public function importPage( + $pageNumber, + $box = PageBoundaries::CROP_BOX, + $groupXObject = true, + $importExternalLinks = false + ) { + if ($this->currentReaderId === null) { throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.'); } $pageId = $this->currentReaderId; $pageNumber = (int)$pageNumber; - $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0'); + $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0') . '|' . ($importExternalLinks ? '1' : '0'); // for backwards compatibility with FPDI 1 $box = \ltrim($box, '/'); @@ -299,7 +326,7 @@ public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupX if ($rotation !== 0) { $rotation *= -1; - $angle = $rotation * M_PI/180; + $angle = $rotation * M_PI / 180; $a = \cos($angle); $b = \sin($angle); $c = -$b; @@ -332,42 +359,53 @@ public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupX // try to use the existing content stream $pageDict = $page->getPageDictionary(); - $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true); - $contents = PdfType::resolve($contentsObject, $reader->getParser()); - - // just copy the stream reference if it is only a single stream - if (($contentsIsStream = ($contents instanceof PdfStream)) - || ($contents instanceof PdfArray && \count($contents->value) === 1) - ) { - if ($contentsIsStream) { - /** - * @var PdfIndirectObject $contentsObject - */ - $stream = $contents; + try { + $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true); + $contents = PdfType::resolve($contentsObject, $reader->getParser()); + + // just copy the stream reference if it is only a single stream + if ( + ($contentsIsStream = ($contents instanceof PdfStream)) + || ($contents instanceof PdfArray && \count($contents->value) === 1) + ) { + if ($contentsIsStream) { + /** + * @var PdfIndirectObject $contentsObject + */ + $stream = $contents; + } else { + $stream = PdfType::resolve($contents->value[0], $reader->getParser()); + } + + $filter = PdfDictionary::get($stream->value, 'Filter'); + if (!$filter instanceof PdfNull) { + $dict->value['Filter'] = $filter; + } + $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser()); + $dict->value['Length'] = $length; + $stream->value = $dict; + // otherwise extract it from the array and re-compress the whole stream } else { - $stream = PdfType::resolve($contents->value[0], $reader->getParser()); - } - - $filter = PdfDictionary::get($stream->value, 'Filter'); - if (!$filter instanceof PdfNull) { - $dict->value['Filter'] = $filter; - } - $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser()); - $dict->value['Length'] = $length; - $stream->value = $dict; + $streamContent = $this->compress + ? \gzcompress($page->getContentStream()) + : $page->getContentStream(); - // otherwise extract it from the array and re-compress the whole stream - } else { - $streamContent = $this->compress - ? \gzcompress($page->getContentStream()) - : $page->getContentStream(); + $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent)); + if ($this->compress) { + $dict->value['Filter'] = PdfName::create('FlateDecode'); + } - $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent)); - if ($this->compress) { - $dict->value['Filter'] = PdfName::create('FlateDecode'); + $stream = PdfStream::create($dict, $streamContent); } + // Catch faulty pages and use an empty content stream + } catch (FpdiException $e) { + $dict->value['Length'] = PdfNumeric::create(0); + $stream = PdfStream::create($dict, ''); + } - $stream = PdfStream::create($dict, $streamContent); + $externalLinks = []; + if ($importExternalLinks) { + $externalLinks = $page->getExternalLinks($box); } $this->importedPages[$pageId] = [ @@ -376,7 +414,8 @@ public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupX 'id' => 'TPL' . $this->getNextTemplateId(), 'width' => $width / $this->k, 'height' => $height / $this->k, - 'stream' => $stream + 'stream' => $stream, + 'externalLinks' => $externalLinks ]; return $pageId; @@ -405,6 +444,7 @@ public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height unset($x['pageId']); \extract($x, EXTR_IF_EXISTS); /** @noinspection NotOptimalIfConditionsInspection */ + /** @phpstan-ignore function.alreadyNarrowedType */ if (\is_array($x)) { $x = 0; } @@ -422,21 +462,79 @@ public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height $this->setPageFormat($newSize, $newSize['orientation']); } + $scaleX = ($newSize['width'] / $originalSize['width']); + $scaleY = ($newSize['height'] / $originalSize['height']); + $xPt = $x * $this->k; + $yPt = $y * $this->k; + $newHeightPt = $newSize['height'] * $this->k; + $this->_out( // reset standard values, translate and scale \sprintf( 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', - ($newSize['width'] / $originalSize['width']), - ($newSize['height'] / $originalSize['height']), - $x * $this->k, - ($this->h - $y - $newSize['height']) * $this->k, + $scaleX, + $scaleY, + $xPt, + $this->hPt - $yPt - $newHeightPt, $importedPage['id'] ) ); + if (count($importedPage['externalLinks']) > 0) { + foreach ($importedPage['externalLinks'] as $externalLink) { + // mPDF uses also 'externalLinks' but doesn't come with a rect-value + if (!isset($externalLink['rect'])) { + continue; + } + + /** @var Rectangle $rect */ + $rect = $externalLink['rect']; + $this->Link( + $x + $rect->getLlx() / $this->k * $scaleX, + $y + $newSize['height'] - ($rect->getLly() + $rect->getHeight()) / $this->k * $scaleY, + $rect->getWidth() / $this->k * $scaleX, + $rect->getHeight() / $this->k * $scaleY, + $externalLink['uri'] + ); + + $this->adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage); + } + } + return $newSize; } + /** + * This method will add additional data to the last created link/annotation. + * + * It is separated because TCPDF uses its own logic to handle link annotations. + * This method is overwritten in the TCPDF implementation. + * + * @param array $externalLink + * @param float|int $xPt + * @param float|int $scaleX + * @param float|int $yPt + * @param float|int $newHeightPt + * @param float|int $scaleY + * @param array $importedPage + * @return void + */ + protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) + { + // let's create a relation of the newly created link to the data of the external link + $lastLink = count($this->PageLinks[$this->page]); + $this->PageLinks[$this->page][$lastLink - 1]['importedLink'] = $externalLink; + if (count($externalLink['quadPoints']) > 0) { + $quadPoints = []; + for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { + $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; + $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; + } + + $this->PageLinks[$this->page][$lastLink - 1]['quadPoints'] = $quadPoints; + } + } + /** * Get the size of an imported page. * @@ -494,26 +592,20 @@ protected function writePdfType(PdfType $value) } else { $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false); } - } elseif ($value instanceof PdfName) { $this->_put('/' . $value->value . ' ', false); - } elseif ($value instanceof PdfString) { $this->_put('(' . $value->value . ')', false); - } elseif ($value instanceof PdfHexString) { - $this->_put('<' . $value->value . '>'); - + $this->_put('<' . $value->value . '>', false); } elseif ($value instanceof PdfBoolean) { $this->_put($value->value ? 'true ' : 'false ', false); - } elseif ($value instanceof PdfArray) { $this->_put('[', false); foreach ($value->value as $entry) { $this->writePdfType($entry); } $this->_put(']'); - } elseif ($value instanceof PdfDictionary) { $this->_put('<<', false); foreach ($value->value as $name => $entry) { @@ -521,22 +613,15 @@ protected function writePdfType(PdfType $value) $this->writePdfType($entry); } $this->_put('>>'); - } elseif ($value instanceof PdfToken) { $this->_put($value->value); - } elseif ($value instanceof PdfNull) { - $this->_put('null '); - + $this->_put('null ', false); } elseif ($value instanceof PdfStream) { - /** - * @var $value PdfStream - */ $this->writePdfType($value->value); $this->_put('stream'); $this->_put($value->getStream()); $this->_put('endstream'); - } elseif ($value instanceof PdfIndirectObjectReference) { if (!isset($this->objectMap[$this->currentReaderId])) { $this->objectMap[$this->currentReaderId] = []; @@ -548,14 +633,23 @@ protected function writePdfType(PdfType $value) } $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false); - } elseif ($value instanceof PdfIndirectObject) { - /** - * @var $value PdfIndirectObject - */ $n = $this->objectMap[$this->currentReaderId][$value->objectNumber]; $this->_newobj($n); $this->writePdfType($value->value); + + // add newline before "endobj" for all objects in view to PDF/A conformance + if ( + !( + ($value->value instanceof PdfArray) || + ($value->value instanceof PdfDictionary) || + ($value->value instanceof PdfToken) || + ($value->value instanceof PdfStream) + ) + ) { + $this->_put("\n", false); + } + $this->_put('endobj'); } } diff --git a/vendor/setasign/fpdi/src/GraphicsState.php b/vendor/setasign/fpdi/src/GraphicsState.php new file mode 100644 index 00000000..44641819 --- /dev/null +++ b/vendor/setasign/fpdi/src/GraphicsState.php @@ -0,0 +1,97 @@ +ctm = $ctm; + } + + /** + * @param Matrix $matrix + * @return $this + */ + public function add(Matrix $matrix) + { + $this->ctm = $matrix->multiply($this->ctm); + return $this; + } + + /** + * @param int|float $x + * @param int|float $y + * @param int|float $angle + * @return $this + */ + public function rotate($x, $y, $angle) + { + if (abs($angle) < 1e-5) { + return $this; + } + + $angle = deg2rad($angle); + $c = cos($angle); + $s = sin($angle); + + $this->add(new Matrix($c, $s, -$s, $c, $x, $y)); + + return $this->translate(-$x, -$y); + } + + /** + * @param int|float $shiftX + * @param int|float $shiftY + * @return $this + */ + public function translate($shiftX, $shiftY) + { + return $this->add(new Matrix(1, 0, 0, 1, $shiftX, $shiftY)); + } + + /** + * @param int|float $scaleX + * @param int|float $scaleY + * @return $this + */ + public function scale($scaleX, $scaleY) + { + return $this->add(new Matrix($scaleX, 0, 0, $scaleY, 0, 0)); + } + + /** + * @param Vector $vector + * @return Vector + */ + public function toUserSpace(Vector $vector) + { + return $vector->multiplyWithMatrix($this->ctm); + } +} diff --git a/vendor/setasign/fpdi/src/Math/Matrix.php b/vendor/setasign/fpdi/src/Math/Matrix.php new file mode 100644 index 00000000..578bdc88 --- /dev/null +++ b/vendor/setasign/fpdi/src/Math/Matrix.php @@ -0,0 +1,116 @@ +a = (float)$a; + $this->b = (float)$b; + $this->c = (float)$c; + $this->d = (float)$d; + $this->e = (float)$e; + $this->f = (float)$f; + } + + /** + * @return float[] + */ + public function getValues() + { + return [$this->a, $this->b, $this->c, $this->d, $this->e, $this->f]; + } + + /** + * @param Matrix $by + * @return Matrix + */ + public function multiply(self $by) + { + $a = + $this->a * $by->a + + $this->b * $by->c + //+ 0 * $by->e + ; + + $b = + $this->a * $by->b + + $this->b * $by->d + //+ 0 * $by->f + ; + + $c = + $this->c * $by->a + + $this->d * $by->c + //+ 0 * $by->e + ; + + $d = + $this->c * $by->b + + $this->d * $by->d + //+ 0 * $by->f + ; + + $e = + $this->e * $by->a + + $this->f * $by->c + + /*1 * */$by->e; + + $f = + $this->e * $by->b + + $this->f * $by->d + + /*1 * */$by->f; + + return new self($a, $b, $c, $d, $e, $f); + } +} diff --git a/vendor/setasign/fpdi/src/Math/Vector.php b/vendor/setasign/fpdi/src/Math/Vector.php new file mode 100644 index 00000000..fe203482 --- /dev/null +++ b/vendor/setasign/fpdi/src/Math/Vector.php @@ -0,0 +1,66 @@ +x = (float)$x; + $this->y = (float)$y; + } + + /** + * @return float + */ + public function getX() + { + return $this->x; + } + + /** + * @return float + */ + public function getY() + { + return $this->y; + } + + /** + * @param Matrix $matrix + * @return Vector + */ + public function multiplyWithMatrix(Matrix $matrix) + { + list($a, $b, $c, $d, $e, $f) = $matrix->getValues(); + $x = $a * $this->x + $c * $this->y + $e; + $y = $b * $this->x + $d * $this->y + $f; + + return new self($x, $y); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php index d314c6de..2127f448 100644 --- a/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php @@ -1,9 +1,10 @@ getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) { $this->fileHeaderOffset = 0; - $reader = $this->readXref($offset + $this->fileHeaderOffset); + $reader = $this->readXref($offset); } else { throw $e; } @@ -206,7 +205,7 @@ protected function readXref($offset) $this->parser->getStreamReader()->reset($offset); $this->parser->getTokenizer()->clearStack(); $initValue = $this->parser->readValue(); - + return $this->initReaderInstance($initValue); } @@ -237,7 +236,6 @@ protected function initReaderInstance($initValue) if ($initValue instanceof PdfIndirectObject) { try { $stream = PdfStream::ensure($initValue->value); - } catch (PdfTypeException $e) { throw new CrossReferenceException( 'Invalid object type at xref reference offset.', diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php index ff8f11f2..412b375e 100644 --- a/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php @@ -1,9 +1,10 @@ subSections as $offset => list($startObject, $objectCount)) { + /** + * @var int $startObject + * @var int $objectCount + */ if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) { $position = $offset + 20 * ($objectNumber - $startObject); $this->reader->ensure($position, 20); $line = $this->reader->readBytes(20); - if ($line[17] === 'f') { + if ($line === false || $line[17] === 'f') { return false; } diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php index 7b4a129b..c921fe74 100644 --- a/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php @@ -1,9 +1,10 @@ 20 bytes). - * - * @package setasign\Fpdi\PdfParser\CrossReference */ class LineReader extends AbstractReader implements ReaderInterface { @@ -43,6 +42,7 @@ public function __construct(PdfParser $parser) /** * @inheritdoc + * @return int|false */ public function getOffsetFor($objectNumber) { @@ -72,18 +72,16 @@ public function getOffsets() */ protected function extract(StreamReader $reader) { - $cycles = -1; $bytesPerCycle = 100; - $reader->reset(null, $bytesPerCycle); - while ( - ($trailerPos = \strpos($reader->getBuffer(false), 'trailer', \max($bytesPerCycle * $cycles++, 0))) === false - ) { - if ($reader->increaseLength($bytesPerCycle) === false) { - break; - } - } + $cycles = 0; + do { + // 6 = length of "trailer" - 1 + $pos = \max(($bytesPerCycle * $cycles) - 6, 0); + $trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos); + $cycles++; + } while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false); if ($trailerPos === false) { throw new CrossReferenceException( @@ -127,44 +125,41 @@ protected function read($xrefContent) } unset($differentLineEndings, $m); - $linesCount = \count($lines); - $start = null; - $entryCount = 0; + if (!\is_array($lines)) { + $this->offsets = []; + return; + } + $start = 0; $offsets = []; - /** @noinspection ForeachInvariantsInspection */ - for ($i = 0; $i < $linesCount; $i++) { - $line = \trim($lines[$i]); - if ($line) { - $pieces = \explode(' ', $line); - - $c = \count($pieces); - switch ($c) { - case 2: - $start = (int) $pieces[0]; - $entryCount += (int) $pieces[1]; - break; - - /** @noinspection PhpMissingBreakStatementInspection */ - case 3: - switch ($pieces[2]) { - case 'n': - $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]]; - $start++; - break 2; - case 'f': - $start++; - break 2; - } - // fall through if pieces doesn't match - - default: - throw new CrossReferenceException( - \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)), - CrossReferenceException::INVALID_DATA - ); - } + // trim all lines and remove empty lines + $lines = \array_filter(\array_map('\trim', $lines)); + foreach ($lines as $line) { + $pieces = \explode(' ', $line); + + switch (\count($pieces)) { + case 2: + $start = (int) $pieces[0]; + break; + + case 3: + switch ($pieces[2]) { + case 'n': + $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]]; + $start++; + break 2; + case 'f': + $start++; + break 2; + } + // fall through if pieces doesn't match + + default: + throw new CrossReferenceException( + \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)), + CrossReferenceException::INVALID_DATA + ); } } diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php index 9a99b002..a98fb135 100644 --- a/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php @@ -1,9 +1,10 @@ > 24); - } elseif ($state === 3) { $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2] + 1) * 85 * 85; $out .= \chr($r >> 24); $out .= \chr($r >> 16); - } elseif ($state === 4) { $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3] + 1) * 85; $out .= \chr($r >> 24); diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php index a4b494a3..e37eaa7d 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php @@ -1,9 +1,10 @@ extensionLoaded()) { $oData = $data; - $data = @((\strlen($data) > 0) ? \gzuncompress($data) : ''); + $data = (($data !== '') ? @\gzuncompress($data) : ''); if ($data === false) { // let's try if the checksum is CRC32 $fh = fopen('php://temp', 'w+b'); fwrite($fh, "\x1f\x8b\x08\x00\x00\x00\x00\x00" . $oData); - stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 30]); + // "window" == 31 -> 16 + (8 to 15): Uses the low 4 bits of the value as the window size logarithm. + // The input must include a gzip header and trailer (via 16). + stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 31]); fseek($fh, 0); - $data = stream_get_contents($fh); + $data = @stream_get_contents($fh); fclose($fh); if ($data) { return $data; } - // Try this fallback - $tries = 0; - - $oDataLen = strlen($oData); - while ($tries < 6 && ($data === false || (strlen($data) < (strlen($oDataLen) - $tries - 1)))) { - $data = @(gzinflate(substr($oData, $tries))); - $tries++; - } - - // let's use this fallback only if the $data is longer than the original data - if (strlen($data) > ($oDataLen - $tries - 1)) { - return $data; - } + // Try this fallback (remove the zlib stream header) + $data = @(gzinflate(substr($oData, 2))); - if (!$data) { + if ($data === false) { throw new FlateException( 'Error while decompressing stream.', FlateException::DECOMPRESS_ERROR diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php b/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php index 3488f990..aa3a2b39 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php @@ -1,9 +1,10 @@ nextData = 0; $this->nextBits = 0; - $oldCode = 0; + $prevCode = 0; $uncompData = ''; while (($code = $this->getNextCode()) !== 257) { if ($code === 256) { $this->initsTable(); - $code = $this->getNextCode(); - - if ($code === 257) { - break; - } - + } elseif ($prevCode === 256) { $uncompData .= $this->sTable[$code]; - $oldCode = $code; + } elseif ($code < $this->tIdx) { + $string = $this->sTable[$code]; + $uncompData .= $string; + $this->addStringToTable($this->sTable[$prevCode], $string[0]); } else { - if ($code < $this->tIdx) { - $string = $this->sTable[$code]; - $uncompData .= $string; - - $this->addStringToTable($this->sTable[$oldCode], $string[0]); - $oldCode = $code; - } else { - $string = $this->sTable[$oldCode]; - $string .= $string[0]; - $uncompData .= $string; - - $this->addStringToTable($string); - $oldCode = $code; - } + $string = $this->sTable[$prevCode]; + $string .= $string[0]; + $uncompData .= $string; + + $this->addStringToTable($string); } + $prevCode = $code; } return $uncompData; @@ -165,7 +154,7 @@ protected function addStringToTable($oldString, $newString = '') /** * Returns the next 9, 10, 11 or 12 bits. * - * @return integer + * @return int */ protected function getNextCode() { diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php b/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php index 0eaa3be5..297edacd 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php @@ -1,9 +1,10 @@ streamReader->reset(0); - $offset = false; $maxIterations = 1000; while (true) { $buffer = $this->streamReader->getBuffer(false); @@ -146,7 +146,7 @@ protected function resolveFileHeader() } /** - * Get the cross reference instance. + * Get the cross-reference instance. * * @return CrossReference * @throws CrossReferenceException @@ -181,7 +181,10 @@ public function getPdfVersion() $catalog = $this->getCatalog(); if (isset($catalog->value['Version'])) { - $versionParts = \explode('.', PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value)); + $versionParts = \explode( + '.', + PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value) + ); if (count($versionParts) === 2) { list($major, $minor) = $versionParts; } @@ -200,8 +203,7 @@ public function getPdfVersion() */ public function getCatalog() { - $xref = $this->getCrossReference(); - $trailer = $xref->getTrailer(); + $trailer = $this->getCrossReference()->getTrailer(); $catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this); @@ -224,8 +226,7 @@ public function getIndirectObject($objectNumber, $cache = false) return $this->objects[$objectNumber]; } - $xref = $this->getCrossReference(); - $object = $xref->getIndirectObject($objectNumber); + $object = $this->getCrossReference()->getIndirectObject($objectNumber); if ($cache) { $this->objects[$objectNumber] = $object; @@ -239,7 +240,7 @@ public function getIndirectObject($objectNumber, $cache = false) * * @param null|bool|string $token * @param null|string $expectedType - * @return bool|PdfArray|PdfBoolean|PdfHexString|PdfName|PdfNull|PdfNumeric|PdfString|PdfToken|PdfIndirectObjectReference + * @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken * @throws Type\PdfTypeException */ public function readValue($token = null, $expectedType = null) @@ -258,58 +259,54 @@ public function readValue($token = null, $expectedType = null) switch ($token) { case '(': $this->ensureExpectedType($token, $expectedType); - return PdfString::parse($this->streamReader); + return $this->parsePdfString(); case '<': if ($this->streamReader->getByte() === '<') { $this->ensureExpectedType('<<', $expectedType); $this->streamReader->addOffset(1); - return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this); + return $this->parsePdfDictionary(); } $this->ensureExpectedType($token, $expectedType); - return PdfHexString::parse($this->streamReader); + return $this->parsePdfHexString(); case '/': $this->ensureExpectedType($token, $expectedType); - return PdfName::parse($this->tokenizer, $this->streamReader); + return $this->parsePdfName(); case '[': $this->ensureExpectedType($token, $expectedType); - return PdfArray::parse($this->tokenizer, $this); + return $this->parsePdfArray(); default: if (\is_numeric($token)) { - if (($token2 = $this->tokenizer->getNextToken()) !== false) { + $token2 = $this->tokenizer->getNextToken(); + if ($token2 !== false) { if (\is_numeric($token2)) { - if (($token3 = $this->tokenizer->getNextToken()) !== false) { - switch ($token3) { - case 'obj': - if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) { - throw new Type\PdfTypeException( - 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE - ); - } - - return PdfIndirectObject::parse( - $token, - $token2, - $this, - $this->tokenizer, - $this->streamReader - ); - case 'R': - if ($expectedType !== null && - $expectedType !== PdfIndirectObjectReference::class - ) { - throw new Type\PdfTypeException( - 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE - ); - } - - return PdfIndirectObjectReference::create($token, $token2); + $token3 = $this->tokenizer->getNextToken(); + if ($token3 === 'obj') { + if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + + return $this->parsePdfIndirectObject((int) $token, (int) $token2); + } elseif ($token3 === 'R') { + if ( + $expectedType !== null && + $expectedType !== PdfIndirectObjectReference::class + ) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); } + return PdfIndirectObjectReference::create((int) $token, (int) $token2); + } elseif ($token3 !== false) { $this->tokenizer->pushStack($token3); } } @@ -319,10 +316,11 @@ public function readValue($token = null, $expectedType = null) if ($expectedType !== null && $expectedType !== PdfNumeric::class) { throw new Type\PdfTypeException( - 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE ); } - return PdfNumeric::create($token); + return PdfNumeric::create($token + 0); } if ($token === 'true' || $token === 'false') { @@ -337,7 +335,8 @@ public function readValue($token = null, $expectedType = null) if ($expectedType !== null && $expectedType !== PdfToken::class) { throw new Type\PdfTypeException( - 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE ); } @@ -348,6 +347,65 @@ public function readValue($token = null, $expectedType = null) } } + /** + * @return PdfString + */ + protected function parsePdfString() + { + return PdfString::parse($this->streamReader); + } + + /** + * @return false|PdfHexString + */ + protected function parsePdfHexString() + { + return PdfHexString::parse($this->streamReader); + } + + /** + * @return bool|PdfDictionary + * @throws PdfTypeException + */ + protected function parsePdfDictionary() + { + return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this); + } + + /** + * @return PdfName + */ + protected function parsePdfName() + { + return PdfName::parse($this->tokenizer, $this->streamReader); + } + + /** + * @return false|PdfArray + * @throws PdfTypeException + */ + protected function parsePdfArray() + { + return PdfArray::parse($this->tokenizer, $this); + } + + /** + * @param int $objectNumber + * @param int $generationNumber + * @return false|PdfIndirectObject + * @throws Type\PdfTypeException + */ + protected function parsePdfIndirectObject($objectNumber, $generationNumber) + { + return PdfIndirectObject::parse( + $objectNumber, + $generationNumber, + $this, + $this->tokenizer, + $this->streamReader + ); + } + /** * Ensures that the token will evaluate to an expected object type (or not). * @@ -356,7 +414,7 @@ public function readValue($token = null, $expectedType = null) * @return bool * @throws Type\PdfTypeException */ - private function ensureExpectedType($token, $expectedType) + protected function ensureExpectedType($token, $expectedType) { static $mapping = [ '(' => PdfString::class, diff --git a/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php b/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php index 56c731f5..b2264ab3 100644 --- a/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php +++ b/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php @@ -1,9 +1,10 @@ stream = $stream; $this->closeStream = $closeStream; $this->reset(); @@ -192,8 +197,9 @@ public function getBuffer($atOffset = true) public function getByte($position = null) { $position = (int) ($position !== null ? $position : $this->offset); - if ($position >= $this->bufferLength && - (!$this->increaseLength() || $position >= $this->bufferLength) + if ( + $position >= $this->bufferLength + && (!$this->increaseLength() || $position >= $this->bufferLength) ) { return false; } @@ -226,8 +232,9 @@ public function readByte($position = null) $offset = $this->offset; } - if ($offset >= $this->bufferLength && - ((!$this->increaseLength()) || $offset >= $this->bufferLength) + if ( + $offset >= $this->bufferLength + && ((!$this->increaseLength()) || $offset >= $this->bufferLength) ) { return false; } @@ -245,7 +252,7 @@ public function readByte($position = null) * * @param int $length * @param int|null $position - * @return string + * @return string|false */ public function readBytes($length, $position = null) { @@ -262,8 +269,9 @@ public function readBytes($length, $position = null) $offset = $this->offset; } - if (($offset + $length) > $this->bufferLength && - ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength) + if ( + ($offset + $length) > $this->bufferLength + && ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength) ) { return false; } @@ -410,16 +418,20 @@ public function reset($pos = 0, $length = 200) \fseek($this->stream, $pos); $this->position = $pos; - $this->buffer = $length > 0 ? \fread($this->stream, $length) : ''; - $this->bufferLength = \strlen($this->buffer); $this->offset = 0; + if ($length > 0) { + $this->buffer = (string) \fread($this->stream, $length); + } else { + $this->buffer = ''; + } + $this->bufferLength = \strlen($this->buffer); // If a stream wrapper is in use it is possible that // length values > 8096 will be ignored, so use the // increaseLength()-method to correct that behavior if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) { // increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer - $this->buffer = \substr($this->buffer, 0, $length); + $this->buffer = (string) \substr($this->buffer, 0, $length); $this->bufferLength = \strlen($this->buffer); } } @@ -433,7 +445,8 @@ public function reset($pos = 0, $length = 200) */ public function ensure($pos, $length) { - if ($pos >= $this->position + if ( + $pos >= $this->position && $pos < ($this->position + $this->bufferLength) && ($this->position + $this->bufferLength) >= ($pos + $length) ) { diff --git a/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php b/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php index b2bbf708..889d036c 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php +++ b/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php @@ -1,9 +1,10 @@ leapWhiteSpaces() === false) { return false; } @@ -124,11 +117,8 @@ public function getNextToken() } while ( // Break the loop if a delimiter or white space char is matched // in the current buffer or increase the buffers length - $lastBuffer !== false && - ( - $bufferOffset + $pos === \strlen($lastBuffer) && - $this->streamReader->increaseLength() - ) + $bufferOffset + $pos === \strlen($lastBuffer) + && $this->streamReader->increaseLength() ); $result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1); diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php index 906cd3ad..15f55497 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php @@ -1,9 +1,10 @@ value = $result; return $v; @@ -55,7 +55,7 @@ public static function parse(Tokenizer $tokenizer, PdfParser $parser) */ public static function create(array $values = []) { - $v = new self; + $v = new self(); $v->value = $values; return $v; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php index 07ab0fe4..3007a65f 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php @@ -1,9 +1,10 @@ value = (boolean) $value; + $v = new self(); + $v->value = (bool) $value; return $v; } diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php index 0f07dc59..db4009ca 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php @@ -1,9 +1,10 @@ getNextToken()) !== '>' && $token !== false && $lastToken !== '>') { + while (($token = $tokenizer->getNextToken()) !== '>' || $lastToken !== '>') { + if ($token === false) { + return false; + } $lastToken = $token; } - if ($token === false) { - return false; - } - break; } @@ -79,7 +77,7 @@ public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, P $entries[$key->value] = $value; } - $v = new self; + $v = new self(); $v->value = $entries; return $v; @@ -93,7 +91,7 @@ public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, P */ public static function create(array $entries = []) { - $v = new self; + $v = new self(); $v->value = $entries; return $v; @@ -104,11 +102,11 @@ public static function create(array $entries = []) * * @param mixed $dictionary * @param string $key - * @param PdfType|mixed|null $default + * @param PdfType|null $default * @return PdfNull|PdfType * @throws PdfTypeException */ - public static function get($dictionary, $key, PdfType $default = null) + public static function get($dictionary, $key, ?PdfType $default = null) { $dictionary = self::ensure($dictionary); @@ -116,9 +114,7 @@ public static function get($dictionary, $key, PdfType $default = null) return $dictionary->value[$key]; } - return $default === null - ? new PdfNull() - : $default; + return $default ?? new PdfNull(); } /** diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php index 8a0cb23c..309706b3 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php @@ -1,9 +1,10 @@ getOffset(); - /** - * @var string $buffer - * @var int $pos - */ while (true) { $buffer = $streamReader->getBuffer(false); $pos = \strpos($buffer, '>', $bufferOffset); @@ -48,7 +43,7 @@ public static function parse(StreamReader $streamReader) $result = \substr($buffer, $bufferOffset, $pos - $bufferOffset); $streamReader->setOffset($pos + 1); - $v = new self; + $v = new self(); $v->value = $result; return $v; @@ -62,7 +57,7 @@ public static function parse(StreamReader $streamReader) */ public static function create($string) { - $v = new self; + $v = new self(); $v->value = $string; return $v; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php index 188fe0da..ce51d48d 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php @@ -1,9 +1,10 @@ pushStack($nextToken); } - $v = new self; - $v->objectNumber = (int) $objectNumberToken; - $v->generationNumber = (int) $objectGenerationNumberToken; + $v = new self(); + $v->objectNumber = (int) $objectNumber; + $v->generationNumber = (int) $objectGenerationNumber; $v->value = $value; return $v; @@ -68,7 +67,7 @@ public static function parse( */ public static function create($objectNumber, $generationNumber, PdfType $value) { - $v = new self; + $v = new self(); $v->objectNumber = (int) $objectNumber; $v->generationNumber = (int) $generationNumber; $v->value = $value; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php index 08928980..1ace3784 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php @@ -1,9 +1,10 @@ value = (int) $objectNumber; $v->generationNumber = (int) $generationNumber; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php index cbf18337..3732aecc 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php @@ -1,9 +1,10 @@ getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) { $v->value = (string) $tokenizer->getNextToken(); return $v; @@ -44,12 +43,13 @@ public static function parse(Tokenizer $tokenizer, StreamReader $streamReader) * @param string $value * @return string */ - static public function unescape($value) + public static function unescape($value) { - if (strpos($value, '#') === false) + if (strpos($value, '#') === false) { return $value; + } - return preg_replace_callback('/#([a-fA-F\d]{2})/', function($matches) { + return preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) { return chr(hexdec($matches[1])); }, $value); } @@ -62,7 +62,7 @@ static public function unescape($value) */ public static function create($string) { - $v = new self; + $v = new self(); $v->value = $string; return $v; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php index 762a52c0..7aef3633 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php @@ -1,9 +1,10 @@ value = $value + 0; return $v; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php index 615ac50a..0a403687 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php @@ -1,9 +1,10 @@ value = $dictionary; $v->reader = $reader; $v->parser = $parser; @@ -47,25 +46,20 @@ public static function parse(PdfDictionary $dictionary, StreamReader $reader, Pd // Find the first "newline" while (($firstByte = $reader->getByte($offset)) !== false) { - if ($firstByte !== "\n" && $firstByte !== "\r") { - $offset++; - } else { + $offset++; + if ($firstByte === "\n" || $firstByte === "\r") { break; } } - if (false === $firstByte) { + if ($firstByte === false) { throw new PdfTypeException( 'Unable to parse stream data. No newline after the stream keyword found.', PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD ); } - $sndByte = $reader->getByte($offset + 1); - if ($firstByte === "\n" || $firstByte === "\r") { - $offset++; - } - + $sndByte = $reader->getByte($offset); if ($sndByte === "\n" && $firstByte !== "\n") { $offset++; } @@ -86,7 +80,7 @@ public static function parse(PdfDictionary $dictionary, StreamReader $reader, Pd */ public static function create(PdfDictionary $dictionary, $stream) { - $v = new self; + $v = new self(); $v->value = $dictionary; $v->stream = (string) $stream; @@ -115,7 +109,7 @@ public static function ensure($stream) /** * The stream reader instance. * - * @var StreamReader + * @var StreamReader|null */ protected $reader; @@ -219,18 +213,16 @@ protected function extractStream() } /** - * Get the unfiltered stream data. + * Get all filters defined for this stream. * - * @return string - * @throws FilterException - * @throws PdfParserException + * @return PdfType[] + * @throws PdfTypeException */ - public function getUnfilteredStream() + public function getFilters() { - $stream = $this->getStream(); $filters = PdfDictionary::get($this->value, 'Filter'); if ($filters instanceof PdfNull) { - return $stream; + return []; } if ($filters instanceof PdfArray) { @@ -239,6 +231,24 @@ public function getUnfilteredStream() $filters = [$filters]; } + return $filters; + } + + /** + * Get the unfiltered stream data. + * + * @return string + * @throws FilterException + * @throws PdfParserException + */ + public function getUnfilteredStream() + { + $stream = $this->getStream(); + $filters = $this->getFilters(); + if ($filters === []) { + return $stream; + } + $decodeParams = PdfDictionary::get($this->value, 'DecodeParms'); if ($decodeParams instanceof PdfArray) { $decodeParams = $decodeParams->value; @@ -314,6 +324,21 @@ public function getUnfilteredStream() $stream = $filterObject->decode($stream); break; + case 'Crypt': + if (!$decodeParam instanceof PdfDictionary) { + break; + } + // Filter is "Identity" + $name = PdfDictionary::get($decodeParam, 'Name'); + if (!$name instanceof PdfName || $name->value !== 'Identity') { + break; + } + + throw new FilterException( + 'Support for Crypt filters other than "Identity" is not implemented.', + FilterException::UNSUPPORTED_FILTER + ); + default: throw new FilterException( \sprintf('Unsupported filter "%s".', $filter->value), diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php index e6011acd..89b90c5d 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php @@ -1,9 +1,10 @@ setOffset($pos); - $v = new self; + $v = new self(); $v->value = $result; return $v; @@ -61,7 +60,7 @@ public static function parse(StreamReader $streamReader) */ public static function create($value) { - $v = new self; + $v = new self(); $v->value = $value; return $v; @@ -79,6 +78,36 @@ public static function ensure($string) return PdfType::ensureType(self::class, $string, 'String value expected.'); } + /** + * Escapes sequences in a string according to the PDF specification. + * + * @param string $s + * @return string + */ + public static function escape($s) + { + // Still a bit faster, than direct replacing + if ( + \strpos($s, '\\') !== false || + \strpos($s, ')') !== false || + \strpos($s, '(') !== false || + \strpos($s, "\x0D") !== false || + \strpos($s, "\x0A") !== false || + \strpos($s, "\x09") !== false || + \strpos($s, "\x08") !== false || + \strpos($s, "\x0C") !== false + ) { + // is faster than strtr(...) + return \str_replace( + ['\\', ')', '(', "\x0D", "\x0A", "\x09", "\x08", "\x0C"], + ['\\\\', '\\)', '\\(', '\r', '\n', '\t', '\b', '\f'], + $s + ); + } + + return $s; + } + /** * Unescapes escaped sequences in a PDF string according to the PDF specification. * @@ -138,22 +167,23 @@ public static function unescape($s) $actualChar = \ord($s[$count]); // ascii 48 = number 0 // ascii 57 = number 9 - if ($actualChar >= 48 && - $actualChar <= 57) { + if ($actualChar >= 48 && $actualChar <= 57) { $oct = '' . $s[$count]; /** @noinspection NotOptimalIfConditionsInspection */ - if ($count + 1 < $n && - \ord($s[$count + 1]) >= 48 && - \ord($s[$count + 1]) <= 57 + if ( + $count + 1 < $n + && \ord($s[$count + 1]) >= 48 + && \ord($s[$count + 1]) <= 57 ) { $count++; $oct .= $s[$count]; /** @noinspection NotOptimalIfConditionsInspection */ - if ($count + 1 < $n && - \ord($s[$count + 1]) >= 48 && - \ord($s[$count + 1]) <= 57 + if ( + $count + 1 < $n + && \ord($s[$count + 1]) >= 48 + && \ord($s[$count + 1]) <= 57 ) { $oct .= $s[++$count]; } diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php index c5329381..a0de203e 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php @@ -1,9 +1,10 @@ value = $token; return $v; diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php index 548dba2c..b32eb442 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php @@ -1,9 +1,10 @@ getIndirectObject($value->value); + } + if ($value instanceof PdfIndirectObject) { if ($stopAtIndirectObject === true) { return $value; } - return self::resolve($value->value, $parser, $stopAtIndirectObject); - } - - if ($value instanceof PdfIndirectObjectReference) { - return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject); + if (\in_array($value->objectNumber, $ensuredObjectsList, true)) { + throw new PdfParserException( + \sprintf('Indirect reference recursion detected (%s).', $value->objectNumber) + ); + } + $ensuredObjectsList[] = $value->objectNumber; + return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList); } return $value; @@ -70,6 +80,34 @@ protected static function ensureType($type, $value, $errorMessage) return $value; } + /** + * Flatten indirect object references to direct objects. + * + * @param PdfType $value + * @param PdfParser $parser + * @return PdfType + * @throws CrossReferenceException + * @throws PdfParserException + */ + public static function flatten(PdfType $value, PdfParser $parser) + { + if ($value instanceof PdfIndirectObjectReference) { + return self::flatten(self::resolve($value, $parser), $parser); + } + + if ($value instanceof PdfDictionary || $value instanceof PdfArray) { + foreach ($value->value as $key => $_value) { + $value->value[$key] = self::flatten($_value, $parser); + } + } + + if ($value instanceof PdfStream) { + throw new PdfTypeException('There is a stream object found which cannot be flattened to a direct object.'); + } + + return $value; + } + /** * The value of the PDF type. * diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php index 1ee20ecf..3d2b93cd 100644 --- a/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php @@ -1,9 +1,10 @@ getX(), $ll->getY(), $ur->getX(), $ur->getY()); + } + /** * Rectangle constructor. * diff --git a/vendor/setasign/fpdi/src/PdfReader/Page.php b/vendor/setasign/fpdi/src/PdfReader/Page.php index 645f8d0e..c2463dd2 100644 --- a/vendor/setasign/fpdi/src/PdfReader/Page.php +++ b/vendor/setasign/fpdi/src/PdfReader/Page.php @@ -1,23 +1,30 @@ pageDictionary) { + if ($this->pageDictionary === null) { $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser)); } @@ -156,7 +161,7 @@ public function getAttribute($name, $inherited = true) public function getRotation() { $rotation = $this->getAttribute('Rotate'); - if (null === $rotation) { + if ($rotation === null) { return 0; } @@ -269,4 +274,149 @@ public function getContentStream() PdfReaderException::UNEXPECTED_DATA_TYPE ); } + + /** + * Get information of all external links on this page. + * + * All coordinates are normalized in view to rotation and translation of the boundary-box, so that their + * origin is lower-left. + * + * The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding. + * + * @return array + */ + public function getExternalLinks($box = PageBoundaries::CROP_BOX) + { + try { + $dict = $this->getPageDictionary(); + $annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser); + } catch (FpdiException $e) { + return []; + } + + if (!$annotations instanceof PdfArray) { + return []; + } + + $links = []; + + foreach ($annotations->value as $entry) { + try { + $annotation = PdfType::resolve($entry, $this->parser); + + $value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser); + if (!$value instanceof PdfName || $value->value !== 'Link') { + continue; + } + + $dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser); + if (!$dest instanceof PdfNull) { + continue; + } + + $action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser); + if (!$action instanceof PdfDictionary) { + continue; + } + + $actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser); + if (!$actionType instanceof PdfName || $actionType->value !== 'URI') { + continue; + } + + $uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser); + if ($uri instanceof PdfString) { + $uriValue = PdfString::unescape($uri->value); + } elseif ($uri instanceof PdfHexString) { + $uriValue = \hex2bin($uri->value); + } else { + continue; + } + + $rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser); + if (!$rect instanceof PdfArray || count($rect->value) !== 4) { + continue; + } + + $rect = Rectangle::byPdfArray($rect, $this->parser); + if ($rect->getWidth() === 0 || $rect->getHeight() === 0) { + continue; + } + + $bbox = $this->getBoundary($box); + $rotation = $this->getRotation(); + + $gs = new GraphicsState(); + $gs->translate(-$bbox->getLlx(), -$bbox->getLly()); + $gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation); + + switch ($rotation) { + case 90: + $gs->translate(-$bbox->getWidth(), 0); + break; + case 180: + $gs->translate(-$bbox->getWidth(), -$bbox->getHeight()); + break; + case 270: + $gs->translate(0, -$bbox->getHeight()); + break; + } + + $normalizedRect = Rectangle::byVectors( + $gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())), + $gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry())) + ); + + $quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser); + $normalizedQuadPoints = []; + if ($quadPoints instanceof PdfArray) { + $quadPointsCount = count($quadPoints->value); + if ($quadPointsCount % 8 === 0) { + for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) { + $x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser)); + $y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser)); + + $v = $gs->toUserSpace(new Vector($x->value, $y->value)); + $normalizedQuadPoints[] = $v->getX(); + $normalizedQuadPoints[] = $v->getY(); + } + } + } + + // we remove unsupported/unneeded values here + unset( + $annotation->value['P'], + $annotation->value['NM'], + $annotation->value['AP'], + $annotation->value['AS'], + $annotation->value['Type'], + $annotation->value['Subtype'], + $annotation->value['Rect'], + $annotation->value['A'], + $annotation->value['QuadPoints'], + $annotation->value['Rotate'], + $annotation->value['M'], + $annotation->value['StructParent'], + $annotation->value['OC'] + ); + + // ...and flatten the PDF object to eliminate any indirect references. + // Indirect references are a problem when writing the output in FPDF + // because FPDF uses pre-calculated object numbers while FPDI creates + // them at runtime. + $annotation = PdfType::flatten($annotation, $this->parser); + + $links[] = [ + 'rect' => $normalizedRect, + 'quadPoints' => $normalizedQuadPoints, + 'uri' => $uriValue, + 'pdfObject' => $annotation + ]; + } catch (FpdiException $e) { + continue; + } + } + + return $links; + } } diff --git a/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php b/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php index 3842c888..35154218 100644 --- a/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php +++ b/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php @@ -1,9 +1,10 @@ pages[$pageNumber - 1]; if ($page instanceof PdfIndirectObjectReference) { - $readPages = function ($kids) use (&$readPages) { + $alreadyReadKids = []; + $readPages = function ($kids) use (&$readPages, &$alreadyReadKids) { $kids = PdfArray::ensure($kids); /** @noinspection LoopWhichDoesNotLoopInspection */ foreach ($kids->value as $reference) { $reference = PdfIndirectObjectReference::ensure($reference); + + if (\in_array($reference->value, $alreadyReadKids, true)) { + throw new PdfReaderException('Recursive pages dictionary detected.'); + } + $alreadyReadKids[] = $reference->value; + $object = $this->parser->getIndirectObject($reference->value); $type = PdfDictionary::get($object->value, 'Type'); @@ -169,6 +175,7 @@ public function getPage($pageNumber) if ($type->value === 'Pages') { $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser); try { + $alreadyReadKids[] = $page->objectNumber; $page = $this->pages[$pageNumber - 1] = $readPages($kids); } catch (PdfReaderException $e) { if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) { @@ -178,6 +185,7 @@ public function getPage($pageNumber) // let's reset the pages array and read all page objects $this->pages = []; $this->readPages(true); + // @phpstan-ignore-next-line $page = $this->pages[$pageNumber - 1]; } } else { @@ -202,7 +210,9 @@ protected function readPages($readAll = false) return; } - $readPages = function ($kids, $count) use (&$readPages, $readAll) { + $expectedPageCount = $this->getPageCount(); + $alreadyReadKids = []; + $readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll, $expectedPageCount) { $kids = PdfArray::ensure($kids); $isLeaf = ($count->value === \count($kids->value)); @@ -214,14 +224,27 @@ protected function readPages($readAll = false) continue; } + if (\in_array($reference->value, $alreadyReadKids, true)) { + throw new PdfReaderException('Recursive pages dictionary detected.'); + } + $alreadyReadKids[] = $reference->value; + $object = $this->parser->getIndirectObject($reference->value); $type = PdfDictionary::get($object->value, 'Type'); if ($type->value === 'Pages') { - $readPages(PdfDictionary::get($object->value, 'Kids'), PdfDictionary::get($object->value, 'Count')); + $readPages( + PdfType::resolve(PdfDictionary::get($object->value, 'Kids'), $this->parser), + PdfType::resolve(PdfDictionary::get($object->value, 'Count'), $this->parser) + ); } else { $this->pages[] = $object; } + + // stop if all pages are read - faulty documents exists with additional entries with invalid data. + if (count($this->pages) === $expectedPageCount) { + break; + } } }; diff --git a/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php b/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php index c33d5678..ecebdb96 100644 --- a/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php +++ b/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php @@ -1,9 +1,10 @@ importedPages as $key => $pageData) { + foreach ($this->importedPages as $pageData) { $out .= '/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R '; } @@ -166,7 +179,6 @@ protected function _putxobjects() while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { try { $object = $parser->getIndirectObject($objectNumber); - } catch (CrossReferenceException $e) { if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); @@ -239,28 +251,141 @@ protected function writePdfType(PdfType $value) if ($value instanceof PdfString) { $string = PdfString::unescape($value->value); $string = $this->_encrypt_data($this->currentObjectNumber, $string); - $value->value = \TCPDF_STATIC::_escape($string); - + $value->value = PdfString::escape($string); } elseif ($value instanceof PdfHexString) { $filter = new AsciiHex(); $string = $filter->decode($value->value); $string = $this->_encrypt_data($this->currentObjectNumber, $string); $value->value = $filter->encode($string, true); - } elseif ($value instanceof PdfStream) { $stream = $value->getStream(); $stream = $this->_encrypt_data($this->currentObjectNumber, $stream); $dictionary = $value->value; $dictionary->value['Length'] = PdfNumeric::create(\strlen($stream)); $value = PdfStream::create($dictionary, $stream); - } elseif ($value instanceof PdfIndirectObject) { /** - * @var $value PdfIndirectObject + * @var PdfIndirectObject $value */ $this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber]; } $this->fpdiWritePdfType($value); } -} \ No newline at end of file + + /** + * This method will add additional data to the last created link/annotation. + * + * It will copy styling properties (supported by TCPDF) of the imported link. + * + * @param array $externalLink + * @param float|int $xPt + * @param float|int $scaleX + * @param float|int $yPt + * @param float|int $newHeightPt + * @param float|int $scaleY + * @param array $importedPage + * @return void + */ + protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) + { + $parser = $this->getPdfReader($importedPage['readerId'])->getParser(); + + if ($this->inxobj) { + // store parameters for later use on template + $lastAnnotationKey = count($this->xobjects[$this->xobjid]['annotations']) - 1; + $lastAnnotationOpt = &$this->xobjects[$this->xobjid]['annotations'][$lastAnnotationKey]['opt']; + } else { + $lastAnnotationKey = count($this->PageAnnots[$this->page]) - 1; + $lastAnnotationOpt = &$this->PageAnnots[$this->page][$lastAnnotationKey]['opt']; + } + + // ensure we have a default value - otherwise TCPDF will set it to 4 throughout + $lastAnnotationOpt['f'] = 0; + + // values in this dictonary are all direct objects and we don't need to resolve them here again. + $values = $externalLink['pdfObject']->value; + + foreach ($values as $key => $value) { + try { + switch ($key) { + case 'BS': + $value = PdfDictionary::ensure($value); + $bs = []; + if (isset($value->value['W'])) { + $bs['w'] = PdfNumeric::ensure($value->value['W'])->value; + } + + if (isset($value->value['S'])) { + $bs['s'] = PdfName::ensure($value->value['S'])->value; + } + + if (isset($value->value['D'])) { + $d = []; + foreach (PdfArray::ensure($value->value['D'])->value as $item) { + $d[] = PdfNumeric::ensure($item)->value; + } + $bs['d'] = $d; + } + + $lastAnnotationOpt['bs'] = $bs; + break; + + case 'Border': + $borderArray = PdfArray::ensure($value)->value; + if (count($borderArray) < 3) { + continue 2; + } + + $border = [ + PdfNumeric::ensure($borderArray[0])->value, + PdfNumeric::ensure($borderArray[1])->value, + PdfNumeric::ensure($borderArray[2])->value, + ]; + if (isset($borderArray[3])) { + $dashArray = []; + foreach (PdfArray::ensure($borderArray[3])->value as $item) { + $dashArray[] = PdfNumeric::ensure($item)->value; + } + $border[] = $dashArray; + } + + $lastAnnotationOpt['border'] = $border; + break; + + case 'C': + $c = []; + $colors = PdfArray::ensure(PdfType::resolve($value, $parser))->value; + $m = count($colors) === 4 ? 100 : 255; + foreach ($colors as $item) { + $c[] = PdfNumeric::ensure($item)->value * $m; + } + $lastAnnotationOpt['c'] = $c; + break; + + case 'F': + $lastAnnotationOpt['f'] = $value->value; + break; + + case 'BE': + // is broken in current TCPDF version: "bc" key is checked but "bs" is used. + break; + } + // let's silence invalid/not supported values + } catch (FpdiException $e) { + continue; + } + } + + // QuadPoints are not supported by TCPDF +// if (count($externalLink['quadPoints']) > 0) { +// $quadPoints = []; +// for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { +// $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; +// $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; +// } +// +// ????? = $quadPoints; +// } + } +} diff --git a/vendor/setasign/fpdi/src/TcpdfFpdi.php b/vendor/setasign/fpdi/src/TcpdfFpdi.php index b366daab..84f68196 100644 --- a/vendor/setasign/fpdi/src/TcpdfFpdi.php +++ b/vendor/setasign/fpdi/src/TcpdfFpdi.php @@ -1,9 +1,10 @@ cleanUp(); - } - - /** - * Draws an imported page or a template onto the page or another template. - * - * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the - * aspect ratio. - * - * @param mixed $tpl The template id - * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array - * with the keys "x", "y", "width", "height", "adjustPageSize". - * @param float|int $y The ordinate of upper-left corner. - * @param float|int|null $width The width. - * @param float|int|null $height The height. - * @param bool $adjustPageSize - * @return array The size - * @see Fpdi::getTemplateSize() - */ - public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) - { - if (isset($this->importedPages[$tpl])) { - $size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); - if ($this->currentTemplateId !== null) { - $this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl; - } - return $size; - } - - return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize); - } - - /** - * Get the size of an imported page or template. - * - * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the - * aspect ratio. - * - * @param mixed $tpl The template id - * @param float|int|null $width The width. - * @param float|int|null $height The height. - * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) - */ - public function getTemplateSize($tpl, $width = null, $height = null) - { - $size = parent::getTemplateSize($tpl, $width, $height); - if ($size === false) { - return $this->getImportedPageSize($tpl, $width, $height); - } - - return $size; - } - - /** - * @inheritdoc - * @throws CrossReferenceException - * @throws PdfParserException - */ - public function _putimages() - { - $this->currentReaderId = null; - parent::_putimages(); - - foreach ($this->importedPages as $key => $pageData) { - $this->_newobj(); - $this->importedPages[$key]['objectNumber'] = $this->n; - $this->currentReaderId = $pageData['readerId']; - $this->writePdfType($pageData['stream']); - $this->_put('endobj'); - } - - foreach (\array_keys($this->readers) as $readerId) { - $parser = $this->getPdfReader($readerId)->getParser(); - $this->currentReaderId = $readerId; - - while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { - try { - $object = $parser->getIndirectObject($objectNumber); - - } catch (CrossReferenceException $e) { - if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { - $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); - } else { - throw $e; - } - } - - $this->writePdfType($object); - } - } - - $this->currentReaderId = null; - } - - /** - * @inheritdoc - */ - protected function _putxobjectdict() - { - foreach ($this->importedPages as $key => $pageData) { - $this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R'); - } - - parent::_putxobjectdict(); - } - - /** - * @inheritdoc - */ - protected function _put($s, $newLine = true) - { - if ($newLine) { - $this->buffer .= $s . "\n"; - } else { - $this->buffer .= $s; - } - } -} \ No newline at end of file + const VERSION = '2.6.4'; +} diff --git a/vendor/setasign/fpdi/src/autoload.php b/vendor/setasign/fpdi/src/autoload.php index b649e8b9..cace75bd 100644 --- a/vendor/setasign/fpdi/src/autoload.php +++ b/vendor/setasign/fpdi/src/autoload.php @@ -1,18 +1,19 @@