From 01d4b863f647689752623420d92bf6c538d91460 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 2 Sep 2025 11:42:35 +0200 Subject: [PATCH 01/37] updated mpdf to 8.2 (requires php 8.0 now) --- composer.json | 4 +- composer.lock | 283 ++- plugin.info.txt | 1 + vendor/autoload.php | 5 +- vendor/composer/InstalledVersions.php | 45 +- vendor/composer/autoload_files.php | 1 + vendor/composer/autoload_psr4.php | 5 +- vendor/composer/autoload_static.php | 18 +- vendor/composer/installed.json | 292 ++- vendor/composer/installed.php | 64 +- vendor/composer/platform_check.php | 9 +- .../.github/ISSUE_TEMPLATE/01_bug_report.yml | 35 + .../ISSUE_TEMPLATE/02_feature_request.yml | 8 + .../mpdf/.github/ISSUE_TEMPLATE/Bug_report.md | 29 - .../.github/ISSUE_TEMPLATE/Feature_request.md | 8 - .../mpdf/.github/ISSUE_TEMPLATE/config.yml | 1 + vendor/mpdf/mpdf/.github/SECURITY.md | 5 + .../mpdf/mpdf/.github/workflows/coverage.yml | 2 +- vendor/mpdf/mpdf/.github/workflows/cs.yml | 2 +- .../.github/workflows/static-analysis.yml | 44 + vendor/mpdf/mpdf/.github/workflows/tests.yml | 8 +- vendor/mpdf/mpdf/CHANGELOG.md | 41 + vendor/mpdf/mpdf/README.md | 6 +- vendor/mpdf/mpdf/composer.json | 16 +- vendor/mpdf/mpdf/phpstan-baseline.neon | 1537 +++++++++++++ vendor/mpdf/mpdf/phpstan.neon | 8 + vendor/mpdf/mpdf/src/AssetFetcher.php | 120 + vendor/mpdf/mpdf/src/Color/ColorConverter.php | 4 + .../mpdf/src/Color/ColorSpaceRestrictor.php | 35 +- .../mpdf/mpdf/src/Config/ConfigVariables.php | 3 +- vendor/mpdf/mpdf/src/Config/FontVariables.php | 2 +- .../mpdf/src/Container/ContainerInterface.php | 12 + .../mpdf/src/Container/NotFoundException.php | 8 + .../mpdf/src/Container/SimpleContainer.php | 34 + vendor/mpdf/mpdf/src/CssManager.php | 69 +- .../src/Exception/AssetFetchingException.php | 8 + .../mpdf/mpdf/src/File/LocalContentLoader.php | 13 + .../src/File/LocalContentLoaderInterface.php | 13 + vendor/mpdf/mpdf/src/Form.php | 71 +- vendor/mpdf/mpdf/src/Gradient.php | 32 +- vendor/mpdf/mpdf/src/Http/ClientInterface.php | 12 + .../CurlHttpClient.php} | 121 +- .../src/Http/Exception/ClientException.php | 8 + .../Exception/ForbiddenRequestException.php | 8 + .../src/Http/Exception/NetworkException.php | 8 + .../src/Http/Exception/RequestException.php | 8 + .../mpdf/mpdf/src/Http/SocketHttpClient.php | 110 + vendor/mpdf/mpdf/src/Hyphenator.php | 6 +- vendor/mpdf/mpdf/src/Image/ImageProcessor.php | 1994 +++++++++-------- vendor/mpdf/mpdf/src/Image/Svg.php | 12 +- vendor/mpdf/mpdf/src/Mpdf.php | 568 +++-- vendor/mpdf/mpdf/src/Otl.php | 28 +- vendor/mpdf/mpdf/src/PageBox.php | 56 + vendor/mpdf/mpdf/src/ServiceFactory.php | 82 +- vendor/mpdf/mpdf/src/SizeConverter.php | 13 +- vendor/mpdf/mpdf/src/TTFontFile.php | 47 +- vendor/mpdf/mpdf/src/Tag.php | 4 +- vendor/mpdf/mpdf/src/Tag/A.php | 4 +- vendor/mpdf/mpdf/src/Tag/Annotation.php | 4 +- vendor/mpdf/mpdf/src/Tag/BarCode.php | 6 +- vendor/mpdf/mpdf/src/Tag/BlockTag.php | 21 +- vendor/mpdf/mpdf/src/Tag/Bookmark.php | 4 +- vendor/mpdf/mpdf/src/Tag/DotTab.php | 4 +- vendor/mpdf/mpdf/src/Tag/Hr.php | 3 +- vendor/mpdf/mpdf/src/Tag/Img.php | 5 +- vendor/mpdf/mpdf/src/Tag/IndexEntry.php | 4 +- vendor/mpdf/mpdf/src/Tag/InlineTag.php | 3 +- vendor/mpdf/mpdf/src/Tag/Input.php | 9 +- vendor/mpdf/mpdf/src/Tag/Meter.php | 2 +- vendor/mpdf/mpdf/src/Tag/Option.php | 3 +- vendor/mpdf/mpdf/src/Tag/Select.php | 2 +- vendor/mpdf/mpdf/src/Tag/Table.php | 2 +- vendor/mpdf/mpdf/src/Tag/Tag.php | 4 +- vendor/mpdf/mpdf/src/Tag/Td.php | 2 +- vendor/mpdf/mpdf/src/Tag/TextArea.php | 6 +- vendor/mpdf/mpdf/src/Tag/TextCircle.php | 2 +- vendor/mpdf/mpdf/src/Tag/TocEntry.php | 4 +- vendor/mpdf/mpdf/src/Tag/Tr.php | 11 + vendor/mpdf/mpdf/src/Utils/UtfString.php | 10 +- vendor/mpdf/mpdf/src/Watermark.php | 8 + vendor/mpdf/mpdf/src/WatermarkImage.php | 72 + vendor/mpdf/mpdf/src/WatermarkText.php | 66 + .../mpdf/mpdf/src/Writer/MetadataWriter.php | 46 +- .../mpdf/mpdf/src/Writer/ResourceWriter.php | 17 +- vendor/mpdf/mpdf/src/functions.php | 25 + vendor/mpdf/mpdf/ttfonts/Eeyek-Regular.ttf | Bin 0 -> 46572 bytes vendor/mpdf/mpdf/ttfonts/Garuda.ttf | Bin 57324 -> 55756 bytes vendor/mpdf/psr-http-message-shim/README.md | 16 + .../mpdf/psr-http-message-shim/composer.json | 28 + .../psr-http-message-shim/src/Request.php | 321 +++ .../psr-http-message-shim/src/Response.php | 263 +++ .../mpdf/psr-http-message-shim/src/Stream.php | 271 +++ vendor/mpdf/psr-http-message-shim/src/Uri.php | 305 +++ vendor/mpdf/psr-log-aware-trait/.gitignore | 2 + vendor/mpdf/psr-log-aware-trait/README.md | 20 + vendor/mpdf/psr-log-aware-trait/composer.json | 24 + .../src/MpdfPsrLogAwareTrait.php | 27 + .../src/PsrLogAwareTrait.php | 20 + vendor/mpdf/qrcode/README.md | 12 +- vendor/mpdf/qrcode/src/QrCode.php | 5 +- vendor/myclabs/deep-copy/.github/FUNDING.yml | 12 - vendor/myclabs/deep-copy/README.md | 49 +- vendor/myclabs/deep-copy/composer.json | 41 +- .../deep-copy/src/DeepCopy/DeepCopy.php | 36 +- .../src/DeepCopy/Filter/ChainableFilter.php | 24 + .../Doctrine/DoctrineCollectionFilter.php | 4 +- .../DoctrineEmptyCollectionFilter.php | 4 +- .../src/DeepCopy/Filter/ReplaceFilter.php | 4 +- .../src/DeepCopy/Filter/SetNullFilter.php | 4 +- .../Matcher/Doctrine/DoctrineProxyMatcher.php | 2 +- .../DeepCopy/Matcher/PropertyTypeMatcher.php | 10 +- .../TypeFilter/Date/DatePeriodFilter.php | 42 + vendor/psr/http-message/CHANGELOG.md | 36 + vendor/psr/http-message/LICENSE | 19 + vendor/psr/http-message/README.md | 16 + vendor/psr/http-message/composer.json | 26 + .../psr/http-message/src/MessageInterface.php | 187 ++ .../psr/http-message/src/RequestInterface.php | 130 ++ .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 +++ .../psr/http-message/src/StreamInterface.php | 158 ++ .../src/UploadedFileInterface.php | 123 + vendor/psr/http-message/src/UriInterface.php | 324 +++ vendor/psr/log/Psr/Log/AbstractLogger.php | 128 -- vendor/psr/log/Psr/Log/Test/DummyTest.php | 18 - .../log/Psr/Log/Test/LoggerInterfaceTest.php | 138 -- vendor/psr/log/Psr/Log/Test/TestLogger.php | 147 -- vendor/psr/log/composer.json | 8 +- vendor/psr/log/src/AbstractLogger.php | 15 + .../Log => src}/InvalidArgumentException.php | 0 vendor/psr/log/{Psr/Log => src}/LogLevel.php | 0 .../{Psr/Log => src}/LoggerAwareInterface.php | 6 +- .../log/{Psr/Log => src}/LoggerAwareTrait.php | 8 +- .../log/{Psr/Log => src}/LoggerInterface.php | 47 +- .../psr/log/{Psr/Log => src}/LoggerTrait.php | 64 +- .../psr/log/{Psr/Log => src}/NullLogger.php | 8 +- vendor/setasign/fpdi/LICENSE.txt | 2 +- vendor/setasign/fpdi/README.md | 12 +- vendor/setasign/fpdi/composer.json | 11 +- vendor/setasign/fpdi/src/FpdfTpl.php | 5 +- vendor/setasign/fpdi/src/FpdfTplTrait.php | 31 +- vendor/setasign/fpdi/src/FpdfTrait.php | 193 ++ vendor/setasign/fpdi/src/Fpdi.php | 129 +- vendor/setasign/fpdi/src/FpdiException.php | 5 +- vendor/setasign/fpdi/src/FpdiTrait.php | 234 +- vendor/setasign/fpdi/src/GraphicsState.php | 97 + vendor/setasign/fpdi/src/Math/Matrix.php | 116 + vendor/setasign/fpdi/src/Math/Vector.php | 66 + .../CrossReference/AbstractReader.php | 7 +- .../CrossReference/CrossReference.php | 12 +- .../CrossReferenceException.php | 5 +- .../PdfParser/CrossReference/FixedReader.php | 12 +- .../PdfParser/CrossReference/LineReader.php | 89 +- .../CrossReference/ReaderInterface.php | 5 +- .../fpdi/src/PdfParser/Filter/Ascii85.php | 7 +- .../src/PdfParser/Filter/Ascii85Exception.php | 5 +- .../fpdi/src/PdfParser/Filter/AsciiHex.php | 5 +- .../src/PdfParser/Filter/FilterException.php | 5 +- .../src/PdfParser/Filter/FilterInterface.php | 5 +- .../fpdi/src/PdfParser/Filter/Flate.php | 32 +- .../src/PdfParser/Filter/FlateException.php | 5 +- .../fpdi/src/PdfParser/Filter/Lzw.php | 41 +- .../src/PdfParser/Filter/LzwException.php | 5 +- .../setasign/fpdi/src/PdfParser/PdfParser.php | 154 +- .../fpdi/src/PdfParser/PdfParserException.php | 5 +- .../fpdi/src/PdfParser/StreamReader.php | 41 +- .../setasign/fpdi/src/PdfParser/Tokenizer.php | 22 +- .../fpdi/src/PdfParser/Type/PdfArray.php | 10 +- .../fpdi/src/PdfParser/Type/PdfBoolean.php | 9 +- .../fpdi/src/PdfParser/Type/PdfDictionary.php | 26 +- .../fpdi/src/PdfParser/Type/PdfHexString.php | 15 +- .../src/PdfParser/Type/PdfIndirectObject.php | 23 +- .../Type/PdfIndirectObjectReference.php | 7 +- .../fpdi/src/PdfParser/Type/PdfName.php | 16 +- .../fpdi/src/PdfParser/Type/PdfNull.php | 5 +- .../fpdi/src/PdfParser/Type/PdfNumeric.php | 7 +- .../fpdi/src/PdfParser/Type/PdfStream.php | 73 +- .../fpdi/src/PdfParser/Type/PdfString.php | 56 +- .../fpdi/src/PdfParser/Type/PdfToken.php | 7 +- .../fpdi/src/PdfParser/Type/PdfType.php | 58 +- .../src/PdfParser/Type/PdfTypeException.php | 5 +- .../src/PdfReader/DataStructure/Rectangle.php | 11 +- vendor/setasign/fpdi/src/PdfReader/Page.php | 160 +- .../fpdi/src/PdfReader/PageBoundaries.php | 5 +- .../setasign/fpdi/src/PdfReader/PdfReader.php | 37 +- .../fpdi/src/PdfReader/PdfReaderException.php | 5 +- vendor/setasign/fpdi/src/Tcpdf/Fpdi.php | 147 +- vendor/setasign/fpdi/src/TcpdfFpdi.php | 4 +- vendor/setasign/fpdi/src/Tfpdf/FpdfTpl.php | 7 +- vendor/setasign/fpdi/src/Tfpdf/Fpdi.php | 136 +- vendor/setasign/fpdi/src/autoload.php | 7 +- 191 files changed, 9003 insertions(+), 3012 deletions(-) create mode 100644 vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml create mode 100644 vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml delete mode 100644 vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md delete mode 100644 vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Feature_request.md create mode 100644 vendor/mpdf/mpdf/.github/SECURITY.md create mode 100644 vendor/mpdf/mpdf/.github/workflows/static-analysis.yml create mode 100644 vendor/mpdf/mpdf/phpstan-baseline.neon create mode 100644 vendor/mpdf/mpdf/phpstan.neon create mode 100644 vendor/mpdf/mpdf/src/AssetFetcher.php create mode 100644 vendor/mpdf/mpdf/src/Container/ContainerInterface.php create mode 100644 vendor/mpdf/mpdf/src/Container/NotFoundException.php create mode 100644 vendor/mpdf/mpdf/src/Container/SimpleContainer.php create mode 100644 vendor/mpdf/mpdf/src/Exception/AssetFetchingException.php create mode 100644 vendor/mpdf/mpdf/src/File/LocalContentLoader.php create mode 100644 vendor/mpdf/mpdf/src/File/LocalContentLoaderInterface.php create mode 100644 vendor/mpdf/mpdf/src/Http/ClientInterface.php rename vendor/mpdf/mpdf/src/{RemoteContentFetcher.php => Http/CurlHttpClient.php} (54%) create mode 100644 vendor/mpdf/mpdf/src/Http/Exception/ClientException.php create mode 100644 vendor/mpdf/mpdf/src/Http/Exception/ForbiddenRequestException.php create mode 100644 vendor/mpdf/mpdf/src/Http/Exception/NetworkException.php create mode 100644 vendor/mpdf/mpdf/src/Http/Exception/RequestException.php create mode 100644 vendor/mpdf/mpdf/src/Http/SocketHttpClient.php create mode 100644 vendor/mpdf/mpdf/src/PageBox.php create mode 100644 vendor/mpdf/mpdf/src/Watermark.php create mode 100644 vendor/mpdf/mpdf/src/WatermarkImage.php create mode 100644 vendor/mpdf/mpdf/src/WatermarkText.php create mode 100644 vendor/mpdf/mpdf/src/functions.php create mode 100644 vendor/mpdf/mpdf/ttfonts/Eeyek-Regular.ttf create mode 100644 vendor/mpdf/psr-http-message-shim/README.md create mode 100644 vendor/mpdf/psr-http-message-shim/composer.json create mode 100644 vendor/mpdf/psr-http-message-shim/src/Request.php create mode 100644 vendor/mpdf/psr-http-message-shim/src/Response.php create mode 100644 vendor/mpdf/psr-http-message-shim/src/Stream.php create mode 100644 vendor/mpdf/psr-http-message-shim/src/Uri.php create mode 100644 vendor/mpdf/psr-log-aware-trait/.gitignore create mode 100644 vendor/mpdf/psr-log-aware-trait/README.md create mode 100644 vendor/mpdf/psr-log-aware-trait/composer.json create mode 100644 vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php create mode 100644 vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php delete mode 100644 vendor/myclabs/deep-copy/.github/FUNDING.yml create mode 100644 vendor/myclabs/deep-copy/src/DeepCopy/Filter/ChainableFilter.php create mode 100644 vendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Date/DatePeriodFilter.php create mode 100644 vendor/psr/http-message/CHANGELOG.md create mode 100644 vendor/psr/http-message/LICENSE create mode 100644 vendor/psr/http-message/README.md create mode 100644 vendor/psr/http-message/composer.json create mode 100644 vendor/psr/http-message/src/MessageInterface.php create mode 100644 vendor/psr/http-message/src/RequestInterface.php create mode 100644 vendor/psr/http-message/src/ResponseInterface.php create mode 100644 vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 vendor/psr/http-message/src/StreamInterface.php create mode 100644 vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 vendor/psr/http-message/src/UriInterface.php delete mode 100644 vendor/psr/log/Psr/Log/AbstractLogger.php delete mode 100644 vendor/psr/log/Psr/Log/Test/DummyTest.php delete mode 100644 vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php delete mode 100644 vendor/psr/log/Psr/Log/Test/TestLogger.php create mode 100644 vendor/psr/log/src/AbstractLogger.php rename vendor/psr/log/{Psr/Log => src}/InvalidArgumentException.php (100%) rename vendor/psr/log/{Psr/Log => src}/LogLevel.php (100%) rename vendor/psr/log/{Psr/Log => src}/LoggerAwareInterface.php (56%) rename vendor/psr/log/{Psr/Log => src}/LoggerAwareTrait.php (60%) rename vendor/psr/log/{Psr/Log => src}/LoggerInterface.php (63%) rename vendor/psr/log/{Psr/Log => src}/LoggerTrait.php (57%) rename vendor/psr/log/{Psr/Log => src}/NullLogger.php (74%) create mode 100644 vendor/setasign/fpdi/src/FpdfTrait.php create mode 100644 vendor/setasign/fpdi/src/GraphicsState.php create mode 100644 vendor/setasign/fpdi/src/Math/Matrix.php create mode 100644 vendor/setasign/fpdi/src/Math/Vector.php diff --git a/composer.json b/composer.json index 65cf6e8b..61e4e4c8 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,11 @@ ], "config": { "platform": { - "php": "7.2" + "php": "8.0" } }, "require": { - "mpdf/mpdf": "8.0.*", + "mpdf/mpdf": "8.2.*", "mpdf/qrcode": "^1.2" }, "replace": { diff --git a/composer.lock b/composer.lock index c66b74c8..a228a7fa 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": "967c0fd288a996c842044c1583092191", "packages": [ { "name": "mpdf/mpdf", - "version": "v8.0.17", + "version": "v8.2.6", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a" + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", "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-08-18T08:51:51+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": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "shasum": "" + }, + "require": { + "psr/log": "^3.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/v3.0.0" + }, + "time": "2023-05-03T06:19:36+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,34 +295,97 @@ "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": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "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/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -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/3.0.2" + }, + "time": "2024-09-11T13:17:53+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,29 @@ "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": {}, + "platform-dev": {}, "platform-overrides": { - "php": "7.2" + "php": "8.0" }, "plugin-api-version": "2.6.0" } 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/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..bc5ce2b5 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,8 +7,11 @@ return array( 'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'), - 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), + '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..1aeaad1b 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' => @@ -37,12 +41,24 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04 ), 'Psr\\Log\\' => array ( - 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + 0 => __DIR__ . '/..' . '/psr/log/src', + ), + '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..8fcbe7c8 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.6", + "version_normalized": "8.2.6.0", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a" + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a", - "reference": "5f64118317c8145c0abc606b310aa0a66808398a", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", "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-08-18T08:51:51+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": "v3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "shasum": "" + }, + "require": { + "psr/log": "^3.0" + }, + "time": "2023-05-03T06:19:36+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/v3.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,37 +304,103 @@ "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": "3.0.2", + "version_normalized": "3.0.2.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "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/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, - "time": "2020-03-23T09:12:05+00:00", + "time": "2024-09-11T13:17:53+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -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/3.0.2" + }, "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..5d3b489e 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' => '27b69b62ea8ff7c01f156d4b8719d904bbc912cf', '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.6', + 'version' => '8.2.6.0', + 'reference' => 'dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44', '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' => 'v3.0.0', + 'version' => '3.0.0.0', + 'reference' => 'a633da6065e946cc491e1c962850344bb0bf3e78', + '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' => '3.0.2', + 'version' => '3.0.2.0', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', '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' => '27b69b62ea8ff7c01f156d4b8719d904bbc912cf', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 6d3407db..a70ba47c 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 >= 80000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.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/CHANGELOG.md b/vendor/mpdf/mpdf/CHANGELOG.md index 9c184e29..77734670 100644 --- a/vendor/mpdf/mpdf/CHANGELOG.md +++ b/vendor/mpdf/mpdf/CHANGELOG.md @@ -1,3 +1,43 @@ +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) + +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 +77,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..cb1c5f4e 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": { diff --git a/vendor/mpdf/mpdf/phpstan-baseline.neon b/vendor/mpdf/mpdf/phpstan-baseline.neon new file mode 100644 index 00000000..7a9bf59c --- /dev/null +++ b/vendor/mpdf/mpdf/phpstan-baseline.neon @@ -0,0 +1,1537 @@ +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: '#^Variable \$dif might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/Gif/ColorTable.php + + - + message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Gradient.php + + - + message: '#^Comparison operation "\<" between \(array\|float\|int\) and 1 results in an error\.$#' + identifier: smaller.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 \(list\\|string\|null\) and \-1 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Image/Svg.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: '#^Comparison operation "\<" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' + identifier: smaller.invalid + count: 2 + path: src/Mpdf.php + + - + message: '#^Comparison operation "\>" between \(array\|float\|int\) and \(float\|int\) results in an error\.$#' + identifier: greater.invalid + count: 1 + path: src/Mpdf.php + + - + message: '#^Comparison operation "\>" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' + identifier: greater.invalid + count: 4 + path: src/Mpdf.php + + - + message: '#^Comparison operation "\>" between array\|float\|int and 0 results in an error\.$#' + identifier: greater.invalid + count: 2 + 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: 2 + 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: '#^Comparison operation "\>" between array\|float\|int\|string\|false\|null and 0 results in an error\.$#' + identifier: greater.invalid + count: 5 + path: src/Otl.php + + - + message: '#^Comparison operation "\>\=" between int and \(array\|float\|int\) results in an error\.$#' + identifier: greaterOrEqual.invalid + count: 1 + path: src/Otl.php + + - + message: '#^Comparison operation "\>\=" between int\<0, max\> and \(array\|float\|int\) results in an error\.$#' + identifier: greaterOrEqual.invalid + 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: 3 + path: src/Otl.php + + - + message: '#^Variable \$SequenceIndex might not be defined\.$#' + identifier: variable.undefined + count: 8 + path: src/Otl.php + + - + message: '#^Variable \$SubstLookupRecord might not be defined\.$#' + identifier: variable.undefined + count: 6 + 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: '#^Comparison operation "\>\=" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' + identifier: greaterOrEqual.invalid + count: 1 + path: src/OtlDump.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: '#^Comparison operation "\>\=" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' + identifier: greaterOrEqual.invalid + count: 1 + path: src/TTFontFile.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: '#^Comparison operation "\>\=" between \(float\|int\) and \(non\-empty\-array\|float\|int\\|int\<1, max\>\) results in an error\.$#' + identifier: greaterOrEqual.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/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,31 @@ public function getFileContentsByCurl($url) if ($this->mpdf->debug) { throw new \Mpdf\MpdfException($message); } + + curl_close($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); + curl_close($ch); - $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; + return $response->withStatus($info['http_code']); } - $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 = ''; - - while (!feof($fh)) { - $data .= fgets($fh, 1024); - } - - fclose($fh); + curl_close($ch); - return $data; + return $response + ->withStatus($info['http_code']) + ->withBody(Stream::create($data)); } - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } } 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..e361b973 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.6'; 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 $remoteContentFetcher; + private $httpClient; + + /** + * @var \Mpdf\File\LocalContentLoaderInterface + */ + private $localContentLoader; + + /** + * @var \Mpdf\AssetFetcher + */ + 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; @@ -3913,6 +3912,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 +5417,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 +5442,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 +5462,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 +5484,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 +5510,7 @@ function applyGPOSpdf($txt, $aix, $x, $y, $OTLdata, $textvar = 0) } } - if ($YPlacement != $lastYPlacement) { + if ($YPlacement !== $lastYPlacement) { $groupBreak = true; } @@ -5547,7 +5531,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 +5548,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 +5556,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 +5568,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 +5635,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 +5684,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 +5734,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 +7287,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 +7301,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 +7848,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 +8229,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 +8879,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 +9095,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 +9649,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 +9701,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 +9747,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 +9828,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] = []; @@ -10084,6 +10149,7 @@ function _beginpage( $this->page++; $this->pages[$this->page] = ''; } + $this->state = 2; $resetHTMLHeadersrequired = false; @@ -10093,15 +10159,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 +10183,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 +10266,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 +10290,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 +10311,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 +10594,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 +10625,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 +11479,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 +11503,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 +11539,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 +11547,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 +11558,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 +11575,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 ((strpos($path, ":/") === false || strpos($path, ":/") > 10) && !@is_file($path)) { // It is a local link. Ignore potential file errors - if (substr($path, 0, 1) == "/") { + if (strpos($path, '/') === 0) { $tr = parse_url($basepath); @@ -11515,10 +11598,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 +13090,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 +13232,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 +13642,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 +15565,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 +15596,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 +15634,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 +15673,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 +16066,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 +16089,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 +16138,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 +16253,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 +19180,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 +20840,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 +21955,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 +22239,13 @@ 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++; + } + 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; @@ -24484,7 +24600,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 +26143,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 +27077,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 +27085,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 +27223,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..11175446 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 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/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 @@ +*&zgi$W90WWl?sJB}PC%WCuZI7XbkkaRUXxy>4}(YSmin zv(~n@b!lt+x>{dr?P_cH_F3E4_VpuV_@22pD7Jm?^ZDcVGbFip?#;}Zv(Gtat^y63droke-AUspw48uRG@LdkSW(8h?<$`e(isqm))Q*;;&D0g@ZR%q> zo4#OK>1pdJ_jLAj_w@4g^9=Ev=9%bO>Uq@jgy+*LFO|lN@)CO4dx^cAz1+S0y;8kO zy{dfv?(_PwpN{|Y`2UPj*dC9Mo5sgsEKk9H7{iE4VT^7V<7F7*ee)OsPX|v&PghS* zPmO1=XViZf!|C1_mF6)n9KSP)umHvw8y_FPHGXM)c)V%6YCL;<)_5dlO(xTCrZpz{ zzngDfzxnRXYd7DydG+R-H($MZ@#eXkdvE65oO3hf(@#JB=u`9w{^Yk$e*NV3C*Oba z=_i*zIq}J(A0~a6nCF>m%rywp=nQH;D&z4AGx`7XA0>zr2$(ov23jBz2(&7(N-9=~ z;~2r1z>~hf3PwGd3!^iDq92ccjc%|jfR>&>ZJ8YtstyTabm0-&FojemRC{UoV|3kG zED>8*T8m|3>zElcqp5UVcv@PxE}gk+(f8)(uPk5w%I9zPS;VHKj0S~eW`^lfQwhi9 z)5VmLJu46jY@vTzO^0j4SpJJLrmB+i^6{A@Rcd}7Ys5C_m&!`+ll*c%%)lPF&qd$? zGtf#^G`w|csTSU9T1DP+Ev=So^>FFXstn{X3qGuWnt5t_W^X3`F>_TWzWc+B6Pq{B zK;oIG4fmn89yF~7b>Ma6-Gg85fph#a;HYf8jDD2u6-*U`3L-&*w7Lj=gf84y<11Du zlzI&u=+ttdNGO*nlnSMoG>G7eLMat%2!^C^pIjzH9~K>|S@51uTSmU!0eN*+f6?yj zrL$*Ml@ulf%Jx_+N}JVUxu~G_On1)Ghxdu<79IM!SnP@}*_RhNZpn+cOHUUkE;wAg z)h|+t9V~OAR=6!YqS4qh7qG!|vB9u(^d0n}--6uAbpWEEa{M=T0UIH(5jYB5L6XA- z0;x=)4Fk9}YA=z{CyYSE6Hux}YWn`g2d-7_YFYB|-u6|ANyuSZR_64mtW4`3>d6&P zKGf3s@ZPrGKI9C|(a=o)_}Gb)2Ol|JQkquLxwewLM`y4BW9k!+k>=-9NlWFjwUvO; z!to!O)vP1X&R-BA5cqIdjL`cEMKp(40wF;rBbO-wXxi7zX8^KP2vBq9db*&hxTdZk z)$gEE^Ti#bvA3_FFe(qfQ?%sL)f36lIdQSZ=#Iec$&InG1Us9O0JJCyq9|RsBqBmD7gA!GM4_af-~Z|Q*x2=-?tlOBjN?Q2W692+ zP|(pMDCozXl0r4E#K&*p$6j@Wev+Y|4bYF2NE#&+igXbX5?we2%;io=s132}KRGaT zJU#vR(1B0Z$5L5G@jE|3AC9l0Ik#@1*{>=KB|Cq_??6vw^Z>PwJqC0aNEJ%xNeK|~ zoXX)C&eRH}#<#4hQIQ%}?G)+h741}07QV1TMQuX;pS@r&Mi*(2C4Q$9% zU4Ae8x)ANBbW{}ZjsU?}ln(o#57GXI2qfL0rG}wrfCPHJuat(3QBej%RFttPYWD1? z$l0?A)Sx_)KyN@lT44LR%TZ$&F=CX!IQ}UU47yG#@P$qZ1)wJlPfvhQArXZKQED$c zN-lG!v|+In6CBg|Y-bv{}#2&Ix9 zHf6jeb;g&`Gg;2k3q=+#5@v?JsBcsvaj}5kmVzc!!EeFN!*3(JBX|)PisT_?To7$d zq+Q5}RC+C=8jIV5*o(D~wWk(Z=z>FLb-&rR_RZF|*D>y=O1!K3dOo=F;^M6bwA3wi zbWC3V?5e-DEq?3ux9d)k(Ev+iw0MFe9!0zfP%0Hfy^z5*YA<1^E^P9tFjUUn4EIhz zBXXx0)#iU~?SHN;I>si@HfBmeXz7rlY%nn*h4ymD+g_B>nH(`ESQw)5F?PM#+4g+H z$j-cDO=w-$3w7B|VSY)hvz*G+b=$LF-4@nV8AmHk%jntN ziSPXnI=|t1XGW7Qa=M5*uk>E9Gxf1YMemBC1DB@~WFV6}iXrdM?yneQ16B_GlVS>ng8U8H?WC zc58VjlHB|iHNLrmVmiM&l&){MxNXOyk>DRGH@w$gaRla{2lMxb`CG#Lkz9>bBETNw zZz{b(jhOmWHT1j852N}-=IS%3hIkZ2jsk&0JOlxPX%BEoggyv>zsCSV-~gHfozwLZ z5mI0sUv3tJk(j`qPY1Vu(lKXkMr3_z{Y$;wS6Ylq!s7?Zf7x1{TpN9QUtFF~$&)>q zi+67=SecAkThGZ(TiIVyIE9R~$a8~B_Q^Csz;upp)8kiom)^s>` zVUBicPyOo0)7vNUh8f4elscsXROCJ8CIkiH=h&J+3c%?Byx>cC7>_0Oezr@_V|UGG z{rxw4-v#8B_r#hpO-p{kn>gfte=sew{=%~Dw^nrGAJ1)kx4q)SKAX^M^g>W-k)Yhh-ki{I6t8EwzT6({3mi7is7%tjn?O4nptmgL z1M2sFzfFHo#J~>=k%s;ovkH7RffVTw{4J!@OjXqGv8Sj(ymL2_A<1sM7iLw4&e2C1 zA@BrvnMkLSlQ(^I>}fg$-b*%b#)+FZ6AptOALe_6-sHe#^uy5EE>kr%IE*TGddmSMt!t91mKbojlyo zaj_9!o)vEPP#wyKYSNru{FYrO zNE74}tlEJk(CEx6Ef&lHcraQ!HdF}QK_DWQf)RqLkTBXXZ@p3odc&I+U2km|BVi*R zU?<_Xr4K!pnL8_cb86#I-tgJ7fW|Lo+}B z8?u@2){oyg{RsZx^HqD01xh{s8j60>beVCV=2NxJkdu+L*pnLD^YbHlxw(e?GrL+} z+nMX(6l_u4zjJj?a$)+07ncpa+n?C}KK|(V+xYb5%JB5GMfF#a-!osJ!ta)D$DbUN z=mVC$*x(Hm*@WNPyU#P-I!gS?KI4%?Dr1~5I7WK?1lr#9aDi981?6hbB-*h#**FE>q|{V@2J*AcyB zw3?z?VKEQh8_=bOw}Qk&xla#nwrW^Hg43ec!Wj^PJ0nxB8U0`(8F3+ftJ4E1>~JE| z+9~BVHF8H={01eoRU)7I$phoJj@mn7%F%iN>j$l!O|QFIy#}L0#DY=6=)^N4*u6Kx z#OSbA;P~)9T#T~Ajh5Kq3>0OjK+@`JDN@?vD{@7j=_`&ay6G<%r}Op-)Aue`Ls+}X z+8Nu}yHXbUli}eh%@zTrgJEo9W5ppv?EfphwPs#ri)s||&KDbw1JO(VhBCiga-q0W zMLXKLHKuO)dnxJz%?-#Z&UPwpbHC4#~%1=zrrO~A&> z?)~wpQqvd9h-@yST)XAqopw)P{V)2{j(`yb*k4flnVz~L<=Mvytwh!P@Pf+zg05c&=ea#B_ zev=S}CLq9|9cVjs47e4Y<>@;b%yf=P{xrCD^e%mZCpG9z(AzNT3Rtas!MOY?&zrc^ zyj#jEO)t)J6FVYd`$|szw*6In?dbedzdf|`w^JqmEPY|q){Di(FAQ(JP(q#DhR0rR zd=4eQxg9YtH#ABuAa_Ms8{{Si2ZQHlOC!d!_}k)P*Z*s<+Wap~pFTQ34O&f$N&=+?4Y!sOFp`)Bq!K8-?N*^Y!S zH(bC^|NQ`xJxcJhAAfuNQ6wi;45$$bcnJ~20yhx78pMJb|0@X$gIEwqox4y=?0$%s zmzv-xEP@TS%?PUPOz@qOynL}|6bq^Mtyy-aB1C~NzGx+9=zzYO z;x1kC;{-YtaM>}yCTI;}CBaBe<}j6)h+DZG>u?jf;1A`BUemk13OT;vNxQZ&eT?#g zdUVSqRMe?2pcLpe4rtT~y%O8YYhOkMzV9DQ3L(WkAC2telkX;gy>S*cwU+QAAHcYkE1viy82F4a6kqti*&;#?B<29}?LPC&Y&SAbc7wLpiAr^(frk|HO zNG$Ok{Gv$Y)QK}Y(H^{E9YxhRi9~mi%vxe!iym8zSEDT}@@*(*dvpS4S-DWenT7ya zD!^ieTy5|Q)Cs29E19fiz3_46pDc(g`ge;a^BO8hfz0r5<-iUijZ zqAV)f0^6gX@I@4de-@rAx=X<&$S?`0#e8f}1+hJJrwb1)hjWM?;-9Iki5?(QNg{|@ zkV}Zi&!b=P&&4b*g1Z{1PiP~%m8@)$lmx{QvVZ%%z4-NR{3i7Y#hON`vnE&#f(y78t3UrtR*}iDg1rN4^QmNQ zqf+^X@Y**#meNO4q2VNoBj;DH5wMa_$Tuz(n(T*&-NcOIL%J;%g+ez2$&YIwI`~)AUA*x@cX+=cv`N>S)|S0 zP++l8r(2UAtJ!p|rSG%ebx+L-@(Pm%`_Ai4&e+kI zvMN5qXU5vEx19K{t+p~cMrm`}WzjQjP0!W``J3^~WWijViPi$n<1EWW>- zeIPvp1M)E)CJR5Gysu!vwvwRcZ%&1_IFiZv=i}#{7X1-uFbUoq z?FKW194(2%ih^igK%86)5s}GCU#-)JlV}y7n0E6EvY?|<^i)75#!iwBY*!_5>h1N= zL_b$Kw}4&f7c8UbDPg`yxpAns)}$g_VlW6PM=AAbs!!FLv?UE`z}3mvpK*XW1_+`c z8UmIE_!J<3;T1}E&PekdtmlZ!f0AXf3hefMY)gr$yhrK?#^H6P=kixu97HVA4{uwU zHTcuEr~baM!P+V`qI&18b7|dEkPIIU$cfFXD=#Z5Zz)*4HmD#Ws;#JeWgKnv$)CP> zN1BCmY!EGU@QN-^D|@2pNLRrInWwAQlrp{YK-Yn1{iXQM%;uCeZ40(+HAT%x_OiG1 ztPcxq$jWK}d_oox*0*00|BTnw#J~}G6wAzODjm-@T(E~1Or{1$=15#}+9J9G{PMB3 z-2WjYH2N%ErCYk+MN-70&LO|W%gIy&DT#0kF@IZJiC-0NX!BZ68#Yh7z8hb=z8G82N8E~@x2~v9sU$US`$VDFO2eo z5zSc>eiB^PM5(0wuw9*<#~$d~-L}CXL~gdsRS?-dwr+qKMCdHs4_rLcOz*!S_g~Oo zua)z`KJL@3pN0FeF3&%Efwm*qKM%*{`5VipEmy-~D3Iv)oS>F=T|vFG-|}@4i<_r{ zmgcBrRnqL~WGbGRBBh*L=PSF3859k$);C1Ug6+oN&#+=`~SkH z04FE&W4`gKRA8z0F!P2&mcRcid^SjeKib^`B!Dn&Dbo9gU_kHWX z+p_tG{*6BjHePCPztq@p89qS{{Bb;^g)!X88r3|;q*W5nqWr`rh7^hU0kvJ<91JQ9 za+ZQtEB7B5GbA6(T=>M|H)gIV4sQ(1F*XgSps@pYR@;d1TT%sOo1JaTYM35CaztA9 z=Vuq5TBK2$;yjl;%UKVABN^au;(T6nv}YI7J;uTK;4? zibRf)$7T?de8x4^@8}b8ef63e9G_`TDWs^`HGJ*O^`ppJVt@B%shqY(w`oc!{^ES? zGc8i!O^7tnHZVTHG-OC0&_ZTGHM1dYW02}Jp$l~W>#zW{`Bt3cURWLCnJ%TRl&Zw( zrVE6@vMh)W2aYKbB0UK1=~0asemO8_3hhF z6+i4zfZ!N&4dTfHA1g+DbKqi6ejdDV4LM-$?2{9bK(aak1!9tiw^H;GIDzCb2;muf<3k~Qwcu~NyD z;%C*1yJA-F*feG=sEb?XGjun{MN-)65+C~TW0Zfg!Bkzh%wIyCOg4ZLCF&XaC!Vt} z{o$WhAyV^Nf$^=wyVl^dP3Wa^NZ5QiCX$GYP&K{5W~_%^VFP*}xrj|Gs2&GbBm|e3 z>rogCJ(|}b^n+`ViGGE_HTdh{pYhL|@!ckLu~aN!jvGP>vWEA{5E7N?m7I5o-6T6pSJk+_;=7j46zF9KsJPlAF&M0nll7NhTDl-BgF%81 z;v`VdFk|o<`~VWjQp4&4Qa5JfqgR6Odk>O{nKOQiaGR}umcLIycv@!s1+PTCJsvUR z_x8U4zh$V7;5S~?+2ilymlBky&-Yr=6-akapnHTW;?V%y4)Rn7V@r9R>Bj@ z5V~&c2IY?T*ehvSE%lq}EwmRRY4V0T+Rn{xCsOB(eduL>5a!T1&d`PIL9PY};x}ZW z&j=?*$7Eap}qYOxj_?dme!v8aa`48RQP z_!%I;0;y%-{Q#H&ft@%5a9~yeWQuCY;{)i#B?kqdrRxeBeLR%}YVC{l5<{L-04tfw zLp@c(1~}yzB=s+9$sCoQK8*#G4-a+1>ZSLgPT3o-)7jc-!^8I_^HAr&MB7#+@)HF( zYpHDdCcTJ6kfp?sK`9mJSUsO`1N8*;LA7b+kxA8+VSy3rYJ=OO+@qswTf(;$QQ65M zL4mfm73Z{x4o-pYHDFiX!$s(0&=1y}1W(QlDu@}=!&2}1gx^D1q(n!jiZaUSRz5wL zK5EEMPR^$rh-KI_uA{$X-W6y_6pgH4N$PEzjJLUG7F(+9;;S&CIs6p7HQacLP|%}Upe|BIQK%dDs)`E}jWViuOuM`SS`f8q z0HOJpwuLlQ&0uZ9;;r6rNNeAe9Jnx}viG@-(}cpK-JY+F&_UDuE5UPA4u0L|sGUP_ zWkjjW2)hebLSaPaeK~>m75SRi1-^PEM_IiRM1;r+lmZY_H?jJQU`Eof>HAYMHb!1c zzZMu67Hf?RS2@(PCY<__o29hifL)2@YthJ;>AyB4`NHDz-I z+#C>81h+;ErKYWqM3%Xm=H?6)=5NZGJ4|p#bUMkOkZ7m~Y6Vpy-tL%vRxq=$uJ?sE z{l}OSbE@S-Fh?ia&}&%l?7GzSU5Bd@C!@0s_-UkgtpmN5@8Dp8?2w<3buCBhSPN&| zRd}@!AGK0ZR(W|=l*$U_Ljzjtn3rcqyV#q4q+l(=IOGW^6o?n1?d&a(T@vj8>4jPq zE!tiS|EN>6_V;ykIN-KpTu842%Et<3!Ci#R{2~l^04%vXO+=NH`f;_IM4rv|oKKe# zd+tsm%|H{PnINvPF10DHU|IQzhK0v9)hx?chMYh)^Zn z2nDdP)*L@8tb=)ynk*7{fq4DBM0x~Cq?5u7isu`UGElLQeFy6@$vIU{_%IWU1u`kD z>wF)7K5`J>dcSko4dj045#)ZoqhK)<0uAE}b#)g^Lqqst9bQixS7jWy+1h<;r(rN* zzPmCzVD3Yu^M(y!#m;Vp)LVz}%@4Y|K7bBJko$+7-5=sxhlY{ha((@!alz){aX7d< zjz2|Cj_W?^8Mx8uA^T;Iy=&pIj_y-M&bBAX4E0n4wTo>4jWU%~LPFfu>R% z7vh}}PCKOf`_Ikxny%A2(_tO9^dcK&_zd?2MtoGkuy4m)f7eAH>8_dK>IOUpIVide ztR`2RtAfG|EiE;F!Q$*JO0OIrM~)K*PcJ~vVhNU_i35-^BkE;F=pv{_g?SNqVn#a0 z{1|9P#hpNJb4?#OS>PfFmb8C1CzMv)i(fNe1$Bh z&##?7ox*QfN+@Ael#r5GQmqxw8VPyxbBzM9twO

$14Fd_O zrdI>bC_yzt*1(tlTsg!il6VfG{CbOvSFbKE?lsPdj?T%6j-JDOR@}=s^5=6v_w0f- zaW(S>^komY=ju5jRtmM9gm%Je(~ae(8!0e1SHEH$hkFK-hJFXc@1!_2iIG&oOmYt%jHyhq>#G_6h0EPL9zU zwO-~VdqHBG5E@+_^Mce<>Zx(~cy)75OI5j*#y@Q4>Xxb6_!*%g84DiYLfe?mZI0AT zPfMb{G{qJf#1W*bSwCLP<@zP@t&fFl}dr-|oQ72% zj9UpDVgwH1xC^RWbz;#(kt=1yG3#C5LaI=yO*icj$v9K}DxJ~fVC4V{7(pm#$rGR@ zZ9r3yDu79&D`;Y*xmXQBD6j!_;`rvxCm!2Yu`!)7ZK9gihl#b7Ww!`0;59kkZ$^x8( zZlizRI{^JvKK`z7A3Gwj64*fu1LVNxex)fQ*#thEXPz{K^TbHFZ)Xe6LzfG8;;+YA zd+7mF298a8MIyd7*4#r6 zLgL2^p+~o|=E&~6f&wRMolNZ$ZU|brcS7p>bp4U`B|8@qL8r#>g+F53baqx#?EC^2Mz~tU(aw;fn0b#hln>RUI)8<)%jXYQ z5B9&sM1aI`HF}~tN<&uJAa_;F`)KGd+Djg2pL6g2iU-;~CfWt>pwvnIs3zJ``pCJUIigaUERSFO6 z`=zywZ7W_}N1d~Dw5w!d+qm+s7DPdLm!zsnf*fryM(^48| zcGQ7c1X)R|JadH^3qmQ^KOB+%=@nf6iO{}mQhSNH9Wsy9 z0TJn+zrVuV4!V`vE+YN&?Uke*bOUHju3nIas3vfS6cXuV7sh|B8g!$sQaS&%aBxpe zjpBc)9R%D#^$Xi4BK#Ce<_7%K$<2+c#o^`#wvo)4b^6Qpk_Xz!oca4J9%v{1bM4T7 z-=uyBF8KQ^$^8%?xh{wi`Z1e`9rJ;ZmoGwkNHQa#p8@+~Ujw?1{S4>^x?#Y60|xXF z_BNlvjuW_pXj;Q~F;o$aftL5@Ye6U?+F;np^7dXekrL{DVP!^Y$9-;%JFMYul~yT7FOW#fH0^#qE2U#YMYJ^i08;vad5bX z%rl2;7Y;{%Y%gIaw-a3R_g6g7PH@V%SDM?&diplke>B&B33HOu0Yp9wpaZ-K7Z9Hf zDxWl=Wcq)o*OZH3F)CLwWL+wPy$zvUB`?%K8Tx+O>zWwqENwIWRwH$Gmex>KZBpkA z$PuyM?R=RI4)TwZjvS(18hoh=HfwE^IPo={P7)?j;>_3CIZIk7=GpfjI&48vjAVx=dzWgM-VZis7u4eAs zl`H0=(8_JIot=^7w1aw4LSj*RR!w%p7Ns`HrD${I_Fek1Yf4ATA}h;^^0cL$7QT73 z>gnBK#c@KmI5v7tvT+Gem#ZsaKL)CRz97_PAhQ1#?IjPiU%q#L#RKh|CfWt>2zF2E zXLzC=H2)r9At=dLCVOA9D+|1jpH6Dcu* zBoaZsz@Ue!V9O+sflHMW{0il?*XZ4 zV~VEp>G@@gBJ3TK8?u*ePh?E51`O2|tc^4i}n&`ayy|1e}BaT?F8?9d!@M@*2|DF zCiK3<_fJ>w{X={4r1lcNef(=^U&E6A`THx(?f-)IWi08RZ?7cnV7Y;Qj1@?Rnu|$5 z)P?t3NWe~^g;E37SMTeq(}RK&xzj|wh5R9QXVrOQVEBXfRW}C$ijaL+xQ4=Gf#0W| z7~A@3@Ku+F_MDfeYP4ydbsH1)%POim^pyW8Ji2M!?VUTnTIJ@FmK;j$o;_$(Oq;c1 zjd6uX|K;x=?R+v$E^E5bw)my)HE$4Hf_KG~vV>P>6C41qel~$)M#ka5XvTfvU$&Pp zliLYy`1>m!Xea%1?a=?3^nNeoY2~uhey5YZ{LaY@a<#i!kZs=HXwwq))b>k+3MBW&VLRW9?IsROe{3&d zCbtu8^Y>Rg&`z+*w^y3mxx5kAe-YPz2?MJf;7~4C3>>N=J_WHWghNB`t7vo8L)kIV(;1UCW5{Z2 zW&CCg8%J7QKb8}{XQ*M5!NW6qpsrys+p{S-gH$)af|4(0BsKok{tT2y3Z6@je6XzZ zqoZY|kA7gb0K#;bxi4h1;|PUF^%Jks`JJE|-v`6AoNf`B&B6q|%q#i`VLD~n5x2QJ zcPL`UsfMDy3NKB?+JY@_*EhVgb-whrf|iJ=mb|&mdddKm zQ`MK2r%&k)njJGgG;DtK;6FDGem~HaQn2v*?SYj8N80*ZFE%eZU)TI%`_cMEX z^?pkkX#pt&Sw*Mm8v;IC0S|6KDoCcCt{$u=yS}Rj=^OYNlnk{yTttZczMS0&J(ki$ ziV=))Qt&0aop|DtA1Co;LU2ezLP&4|t0Lc_iHR^4{2F}?>2rH?l@vh98}a5b`$Ka5+Gi&K;I%Y$Dw2ZJ#|IdQW=qzm*5x88p&a1ZoJ+O(+eRQII*u&#Htb34GV$DiDP=9iwwWoilp)YzlbTg$wh%*Yef!9e}A1H z+v7UL?h1-^c8m7BcSJBp^y^OgHd$HomIdl3pl*T(SEnV30TNRro|8yTl6_=Bt%bN5 zsShNR&@nsvN-Bz7@wd?}bEd~B6aBrEQQ=;BiLp{)bZ~7R^_kZ+VR&L*H}VJw%0~Jl zkLGqwqY|+H!sC^>tCOQ=xfeV(geZT#Bxp&lry`@y^l4&6c-Ja`?JmI7#r6YN2uKDC zb*);(_TOC!xPBnw|HpIqWf(flWK&x1mn2`1J$j3ALx&-{1yq4+f=SolVI>T-O zbj?EWy8yPvbJxa4(P7bR_kSmP?e`GT+veXH=qA+7o)9RYcP^bG*M>}r$?8$JDtj8$ zS7nctjfsjvuAho@B)%<3vjkPz6qp`T;W58)Wt8!GaRi139Zt2%@* zDDWpbMt+@42Cis%{776V2d{a}iPlO{h?g$4mV*Ev zqYWG$0fGrk{(k_08QrnFOROapfdD{t;XMG7tQ8i)06-0}r5pa8zQsNdN0tx^1{*=1 zZKFpbADs^izB4O@Q>m6JHM$y0(XEH@XUGE&;7=cV2&sk(T?-3cSv6_fja`ZjuEu&p zaj6lvjP0Y#Q4V;zZupl6dUt|2g!(^>F=B@9?CKKmm1s+^#-^LAKkqhd4Lq8qBkfHdPHDDsBX4cL^)F){pIC-edY0s;;3`v zpu9hB5ybw%O?BR$r1OQ z4}@WKlR;0+Xm7$ka+%X$bVQoGH6%;>@`z+VFNI)9&( zPl~Mib1DSx6O6~xKR~pHXnw#H{lnOfUK+Ly|IZgfSK<{;2fx-Gt_KTZ1U3KbjxVB# zAz$~$%q`Db|9izEB-5A^Hi>>E^)YIn*yb|Va7rpI!x5!2_*aTPd*|o8{_M(1KjqZQ zD!gdN$tiv`6EEL=h!H(IlDo;ZSmA1n$j;MMrdBRGv48domr^@vR=}+Ef+c0$|A*aU zF14%IHYZPP9y5LM->M>$jL}dODWJ#Shh62Nz?HG;-`;GTH`3F%`QDHs9K9cnC?K%-y}P#+h}L7SCMzeEoNC z#bi>h?360%vts=1eABT%?Kt3WZ_BjR1xZ@lyLj zEe205p_cRSxqW_-5VJzppTF@Ra#3eAL$*e zd|)SD(Y-x;)b2|BQYF#!$#f`d$7Vn!CGni0f>aEuRSZQRaO4X$SyCx{h{Q@LtOy6+ zCqzLmfgcT?kzAzKsaU&DW1kK^8S~7H8Sk!JcWvfOlYOL}#iD7FG3oThmNwJTbviWA z3N~4}p<6h|DiDs3+6KGeov6ko*cQWPtKvLsJj;R#41_%4lh%1dT$~4Z`#O7&5~78i zpBy!Q4fYKl1bO2!*LLQo$nc9(sDpw6OhQc|aU7s5ixxrQ-J~TP{h_TUwdK*)?z0ue z8wNt+3d>UMM2O1k&(y`oN8|(zXZN0666g4uVtQVDc59}_+SizDXf=k>sevP}{QX$X z(#P`-z1bAt5~q*vh^cvOfxWxqLfO>vhaMgJ(P>6V;lTc6kBZ8K`m-Mq*g<={kov zsP3RmC~Jf*x;sMG#Km-s{lZ)k&?q0J3y;!kV6;FUN4(yBaFI9>AmqJ$lFBD4m*9%H zGjJRsJes~dbKbg^*w`g&=VdNWpS>nCqpwpJzHCEA=9<}yYO|-e)c6M!*C(VdHFf*f zG)HIGwx=d6E~XZyuQtY2MnqJ`8CR!g^<*SfhlEroW%Lwfv_$86`_G+Lo#&k!-I9?} zJ&l+N!4zCcuV?>FYPCT+K^UQ$k`~cI3Wz8K;e>in&_6)f2#H=FAt5AIKm*+&3Y+O3 z72^bPXoh91ECbVOlS1doVm&Ri@uZ!mZ0uMI3$Gc~86ib7FB{eX88hcFp+@G~|HAbe zeEVPc{r!pd)>hKkSyC_7CDcMA)qI4sN58s`T#x74TUtszlLS!9fz-lk`Xt1QNZg$R zil_`YfTS?M0>A)F;{S9(SnapBXm)i`!tzr+i=&)V+R8G9*F<(b)}Q>J+G&-hK*-v9 zDFYonG9m|@rv;0I>M)7M-WZFOzWE~7?*649sK4beftlDP*Z^^}c<#ApDqt4S7u*}F zoJdukuzKw0&y1U(TIQkOOii>M93yAE;5TX$s{7nHwBpwl+$ZP)bNxyOR4f7S`S6dD z;Y(LYkZtrSCXIaGL2FjPAASZui-#d7^@rcV=6CXU?{{hNyVtpbmsZ@l0wtm36}Z!6 z(*(K)VzLY))Ioidr9fawfoCAKq^N4N5104g3bZ#K6+`{=exYvc_SnaumLzv$sZ-QG zQ#n*I%mQ`)DZLe>%?YygVc>sc2nt{o+9Fsf7!d3d92Gn%xF~o7Xm@Wt0-E7ZClgN6 zpDqxug~ySs#vy13A3 zaFdLYxVVpfXJHLLl}X)bnT=55?kbnKni}oJa=0#aql^v`nF}1b;zXf~jm*VeB6UNt zwjMH9cet?ERm%MqhUUNNd<$!-n~PlHKK29Oo7kP&WGf2+h}=vk?VuyL)diimRY{>Q znJc~oeYv@zc9bgKCPxNQ_6b}2I#ct-`+R}KnA9|3jIIuL7k))4G_XP`nF z5-Xs8h<7mhMMK?^#{RWens@IV9K@e(-;C6QgL`*3UtQYL+udA;j_=y(t!5c-?^6ew zgG2JE=)s!Okl^MuyVYJ4^7RG}1cZbA7WU7IAbw#7`$FZwtB^;Q%1KF>7Gy*#ruFbp zCoK)OmOm4#n_`*Y<44y&`|PunGkiRYcRqXM*=N~+qoZBS>Z4;t)R^fst{pi*M?H$y zj*Q)5CRwjIwm!`K#C3 zI;Kp~N4~LZ_ul=_W(~i1sk0-`N3)=2>C&su1Ky6}PmkH!Rno}OOn^Uq&U zdnG2m($X9kD|XmHp^wSv9pgW+pTX$h*Gu(ebf|471$-O;-1r0*9!q)>Mi%?h9f)0U zcVr*+`yphT?djp~zjn>4m9oROvyBlEG(9cy3DdD-zf%N;(JqAk1-qtAzSRpBEbyH& zE$VfuK`K?Ll2c-$-L|0Lu?2;GBVO62@wXwq6#y&WKmbcFq`??MGTc`~Q&LhB1zKLt zQ4N3*5wrkNDPLeJsa_~r9H;f0k`Pr^Q=?Y@9Y4NMZc1G|bB4$wI7C-Hr=d|BCY8>a zg`CbHIdIhgeg$7yRQt|H@296B4ZPkNUBPsVjvgJndTsxRR`<%|*Hs={wmotLCG8vS zfmv`^y2B0tbg(3m%9R{&X4)`>xAYNE=c+_nUB`3%j!x}M&z^aB-`$yygQISfegTq0uQ!{5+qHbY){JZoxRE}SBxw7ZsE-$Antsd@B`N0S`Jjq5YrU)p2pNGR~gs>W+?P@k{l&*-H7`g57%q&ai1Ih>n?7 ztf2c}gTdBLGbML!0EBXopmF>*yAkjSd?f`22dM&J2&4cQVKDL{bKqaTu&;%O zT;0gJj=mpT>^Wy}Q(L>OU0`5zMrmH|^l5f>H4Ek#EkyqQC1ux6?e|uz9C8v9fwrwb zbse0Qq0?>I8Wi-!nLEx-Wu+@uIy-f=r>8YEWE&(0Pn|xM?|*(63^_a%bBui#pi=VB zBL?q*LlFmx7Qhn;p%7L@6CFujR6YLr8L7klE_F z!+V5;p*g$&)s(_}^rKZv*Z|X$La~EByrd*1#?5_w;WES>gxLX88f_H7cX7f z+~Skns(&Q9W`Uny?ZWT~D{I#DHsHqeYIerLT0g&t@Z22cs7-!Bwjn=%il3X?%$d0v zD>Xj)NaNA%sotk7nldw$%EIE5bP5s%NB}Dcb`3xc!EsW7L|_E4$!b=|D2Y;3LA4B_ zole0&VR;ejyuQ@VHZb7uos}fmLDgqXjaXur%kgfTos;0g2RAY>v-}kS-82I!@Ia3VTOItIuG8xk} z)V#-aeo=MR>oY+v+s4P)e*ug%r!C|@uovH3A7SQj0vCV-kM1QfM@R^08qx@V9qcmd z?pg}voi5my)L%4D?In?TdxwRJPl#q`*Da2XjkK~(N&R8TlCD^I5zTdEbp08dyWO8v zzG7$l=;6(qX3j`YTeE9$E1_r&0OP-#8a`r)ht7rTx_6dM1RglGFPenkO zuwn`k!?8GkLtui01Nj0ZssVEH0)Y8X1*b@aLJo9amfRE31A_Svn|=EpZ8lUi$D+ByJJW5J-Z2PsnFhAVMsXsd#QQIHeeil&$g?qj- z8Y3eaMyosbi=$&&T2D_x!oa|Mm8+{;YU-_#54<5gU&U#hU)T#UcgQoFH4aArm@?p< zOa%6m5PFb9f&m5WZM^5GG_o0^D+6N0UR$?a`v(%(*eoe5Y;I0Ywi$6~2JV$f!Pdq- zvn(XFcF~cIODrrxLe2nj60214uC9rRZ?9i}!qwG5JU9RSkDQ&u!m6uMl6)lLUf4i&gb1?qKmI92##z0}8m|$EHabF?Ixdwd%X#$u5PQo{$M&{1wx$}dY z<`unOG-Jc2fx5-@4ZBY+Z5fTvcYdkZ0wZv7xqvn zA>Pq3b;>yWhgkCvG)@$F{W)aT-f-DUEDh z*~7AZeLEjQNxw7r+aGqHOM{1pQe$2Y++JO%S7P_Efn;Z`RObUv!vM;_egmFT@KkuU zSVW7&yP0)&(EK9ml%%-0y(27?Ifza_f{wu;=p;URP9(gFacY{QqO=&@99_n&rAC)i z%vz$sW{lrqXRzL6-!jo)$VUl`0K_NrBD2!7zGwz37+Xz^Ip7!V-~l`~7d+RmFD}+- z0!&@5^f_kr{{5!*oTCX9Ww5XAAMWl`r&d=N&DanBF9a=sA=vF~DBw&@_Ut0)l#;3( zco`BYIUWPL5dzYnmVl~Kur*R7Ds`~5K-6y`!MSb-Q z`9`B7Y=Ru2nF|dM8?Xkq)1hnDy1PxEw!x=#XlH=`^y#fFwL;c(?96i-&HN%f2D@O_ z;AOig zT{;Cksz(9eOUE(nA`IZy9V(%K3_%6~Ck>zgl#@ev>6TJgZs2oiX}+5Fj>}h8_IiwX zU4465hd#2c?YZZ`rXDTCKkVPXZ(mjxq6-U=1$J|-t$p;-l9IZ`@rTJg2giS8KY)?E z0P_$_1iu!RWW=tD0NXl^9@H$1Oyq{6GwiC9(@%P-6B7%JdlM3E z*BAf1t>4|<*|~bbk;4XqxA#9^L=PW2jVeu_^eyu7uB?oSwX$NYVK3pWJp3#ESNC%J z9TD4iocv}yxIN{u*4CD0L$;IC^6nGIk6j5QaUq${K0>BTr(L9(cu%`rGi1kSMOA#yBuCWk0iN>ESUim|o|*IFKCVq)gan?2ixqU=vHD^8-LC}xvk zZ;-#WRp;X~W;i$`B|@!v)My8@obUpCu;l+Xv4rP^ivhR%idU-VJUABmUtUBXe*+yr zCdntJ;je+a$m$kfV$kQK&Gc7e&KC6TF$sZpCB%q+fSzEl4bc~{_|bU#>J*Gpypmab z{)_?d{QY+{qa9FvVsz#Cx0zEGE7#LGgt{XD!)*AaJ&BCLZ$+@0gWro_?G0}r^$7DA z!ka$4{v=-U1QiBP{36Snqo(iB6F8AV&*KEdtTufIJ#~$L!xjV1lw2%OB)Y#GBb>pB z5Nvf70Xu^n^Bn%bqD&0Y-WioSk<+3w=4UKi1Y1lcLZPecO8gaVilm~u!<{`C3!06Z z7F!nIzac0{D9Wfr>Ro^p(HeXee>`${e8V^HZs{3G$=QZ~y!-pnqp172lI0#VyLx72 ze7Nd!h5Vr%5AQ))$3}YqN+eh^j@kEM=1|)Jm!Nuq!BJ{q%NnCuD$i1E-%SulA zK{(;%tC0YWv{I#R$>}y5yWpUMd-o#8Wy=odp*_{rgM(w`Wu@L;%~v;CH#Z+VxNTdm zzSMv2(vJ2H>4u+nUfTUQK#afIcc8uFcw(ZZhezk!pAM&_IJu-{Wd6_o-PaL3cxEIs zi>Bul?)YWaQvbPOT6cHkjyw=4azRG{(@Vg|Tn~^t5}OAbCL@#)0(ej-z;<78Xjm0Y zh=7fJo;tPejT&V0_?P(o{{E0Li6k{`#mb0?jOX!fYH_r?`;0hiD{#~O5)$vAB}@MC z&O3WHZr>iMm&#W4K0LE^VXz4t>P4 zYV$LrE9qCq^z@r!Ai0#F5qRx9{Qm#7_vLX>UiaST%nAddB7(9Gs37|$ptu4v0}28H zD((z0z$grpVE}PSG;xnxQ?JozbIH~ko1|uI(xgk$#5QS?CT+I1H%Vifw%NL*Z*Fet zGQ8j4InUz^h|%8n{oK#{$16T_p0obe^Edc%vWoem`(p>OY-x;_#EB93-W+jGCi>PT`yL4&e zgb9N@V#1;Wj@w@%tdn(-np$HVR>WvDhCxnADI_x^Re=jGrz9_kK4e{^J3^~n#l`#f zE~(?QA52SKx=gRv*WcJahu>}J@4xjxw|o5frghw~w_$05dF?v1UOD8`{KZhMw2och zMWS(1wDBq9iz10dMnmPzI%oU-TVtYwYwKP=Ik=>>WbD`_OVAPd&g$y8c$@8bUS9Bp zts33V`|*Hm>XfnA?!|{1bx-kV!)!y5T@gCzd9R-CsIWCt@uW^5s5xHp5{{0ONI^8jD zimt1xu8E6tZop|!AM0v0XN{@gRy>d(H$t(Rh7@>z&n$eQ&#h z3?(H1aPbpg!_d!$en-%>w2}wTr3j`)O_Wxr5CbTPGa4>vW^z8iWWs69g87r8svgp7 z^Kz_ZH;m7{zp^qS!q>1pY(~B@Xtj35*l{`8llSF&JehbStID=Md;Z+MUhvF!LD$JY zFkHeKC9Uchv}D-CkYJ3uOqk%L;i;RnYm$=q!7)|U$27O`#g5jXprs8PgJ;f+j&3mg zW}5HI`E%#>9=~yG_9_0&(EQwedlxPA9lCvDL_uLaNvA<~STofafqq8PfgY%3T}D>clhbF6Yn&Myt2;b6&Rnsl%@G${Som^hraBIm zo`ZJzp`nY~_r$I#BbZ0~rf_NByZL5czT7{h-*vtZ)-escsDk)Rv1d+w?uD*+JgcN6 zE30GypSd7g-ZFlE+0X+0TSJeLp6St<&o2wv_Qkd;FR0jobriQg8Dl~kpivjce}n8f z;2jJ~Nj*6tmM#r`b7h6ARX`Wpi z8rs-+X2+B60x}H z$<~uq8$JMKTZTT-GkrYm7e#NzpFH?O?a)O^a(Z4wTzyn?PIJjW@1dJt1P*vb%NL#*E2GZ)%!4b<)(S4Gq=R)wMZ! zgH26Mp`i=k+o02BWK0d8HX*sLe)n$B(lqoBeF|u?jYdo0lu@9M4nCFU&{K#yP((u+ z`J7QoZzq!WUna@v95be(;=XS`8u75UvnwGXBXeMLQ_FXgK#`9}^Rzj0q8|ybs`4xi z4`17~?dAtHdr#c8-dR$To%6wmbNsZ`)Na3d*Qe&F^863lf=LPG981$I1BmYOzh93);+GQl*?5Ncjh*W1^$POn?Gw6`-jI47r| zV4+SY_|h>kZ0tPSqOdSW+p|^n)U?J8d-uk~W~Of%SeZIu?hT#=B|3d&eYL8UwW|$le|>uL<{_|ApLal#+VTjEfTDNuvKj)ioqEqOBE2z5NeHdr~?1& z$Tovv`^~?230uTguiCwP@4k%md+z?}Pa~u5+2a0i?b^b^KYjGp+w0bql>F!?y?r{} z#(sN~hHJMpX3ztV@bJFA&zhRj(kdz%8szVuZ5=taHTT?Yv1Da!+O%tTeM50^TU%1n z)TyPk9)&$RI`V_3({t{`{P7qifH#oCvqkD}LX=SohFCZQB}kGv27Yqz$zw@L-kx>q zjC(pe4<6cjcR6TjaEx&Xf6LhH@JU)(visv06*!*`{&Y%aHr`=WKyq%DWH z8iP)5nPIVJOGpTpey?(_? zU#(|lquHF3)7nb5z&-S(?jj_SLq7X7kfXgmLD1yX7|B6B1|Pn%t*UaulgFYbZ{4z1 zs?32y2O}azQJILev@JPvd8MK`AqyAsmAb(lJJQl@m$fG^*J`;^dwjn1^sd`hYI+nc z(qh#4QXdQ43$Z87BC$_NOqrH)eH27dZ-CNIfLt)krg4i%+0<55W#Uhi`F88ChoE=E z-Yl~Oo2sf`2(7P+n6Tm%KVb*=^ZD0B+PsROoV>=y@(MooV2BBaSn=}9Jq5dtbkE^! znGm6S0edUO38VB@w*SqU;cpj+O4AzhG#lC#87nF7;(d-B=nz9+3CP`!}^Wjn9w4#6=qrqBz;~V#Q@2<}% z2#YB+o7XlKY*@G;p=`b(U6;G2JfWh%_o*c{(^iq5T&``qm~$~#`>HPI$f>$pDoZSr zGt#4q=f`9&FWl&;`A&iBSN&_YZLFQrwB_OrdGqGy7S1l#{F*9gUxWF%WX9pO7sQ(h zv{(1?y9mAu(Gz_yc&~i>0Cg_jt?L-l;r$KFFzNhQ%>0rIu=EGwiY{7qx3%pls}0{# zS+zY}f2?Bv>ec%z8mp@tQEvz8CE-mHViGolUFW0qB ztQ|isJ2^R&PkvluC>sHFN{SJ1-`o89o13}j4qeAfFY#5dTNe-O9vX_miBsbJ4A3Mu z#*El={PBXUtTIbx)&gH+X;x-wX=YZbJ~7K`%}TdgGP10e%nVy8;5`VA?g2-|zPD+L ztfPE;dVMcur_IPnjnsAgJR#paCk{)~{NfhfnV~eCk`(va>`=uQ>qpeMa}g+5*t^wu(+vwLUc<>ejXmK zadw8kqCcCg8)+PLuWZAWXWpKo`at*bnGgJ^trY)`9SzVS|lxkf;_FdT%h&w&?KIm!0`L8q&aGS5&H%tHH^N(wPaj_6k;qB7C zNBK@X-h-Hkw@wOi)I}ne{^dup#9J0ow10%UMPdQHWlft}@ky+=*PeK44WA#qAUpli zvk$&z+x}R0_v72FuRe6?ne?2J$$b8rr|(#Mn$Q3J`+WXsZQYjV_Ei~fikX-1`yuZr zyzqqM*%PZ*pLo{s#Dzla&a~vui@C{AwePttk8snGBfc+zowAdWo|3?V5N0^z8J|eP z#!EUVHxg%$s;Sr+iensQs{ZXuw`SVQX0OWmFlW{5vZl;S`w{i5(?7Yk*V3Gs5LsNH zODHZJJGQX+!nva2h=ha*#WQsY%PSHRDwbb3S5O=Y*tM`;^wj}K6|k|xOnrxstB2vQ zz3>nIgzpCJ(*yCk@nM&Ie4B5vW_1|$vkt7p@i#?leLwhd*u+b}y7cdKEIwx>7j`;# z%A9p$=lmzr1|fm5-%ajXKwdkv*y3@ECSib%c;{O1=dznRh4)GYI&u2wO4V$e$i}-I@t?q7wAER6@^3oB1 zi6!bEVe|D3EW+5sBJ|<7G%P}Q2i}@#=26;EcB8hLt<*k)uK_#+7F%$he>k(@ZPYmJ zQwVPXt)DTQ<|Lb~%SZeeOVD&7zHDf%&c@<2O{~E13QN`P#kCUo11w$FiaLobTbqq? zPqHH20p!hQ5e5t3M&O#uBD9a7%%9n2%^N%e;Q`%x<|O`u_iuqm#J@(}Iq>W(i_kpB zGj)s&XfO>kpq}O_R;1mFcE4nG#CwrOxF2F|cnj(d&0pDkalL}mshU_n_*%rb<86(% zQU0&2Rr4;(Mg8eUe5)JRT3ppAbAb3h+GXrVI%FZ_K(bIS$RUGdflG^PEMyg@%7J7N zcmc1sMVw?2c#$jyA!`H66nrP%kGRmLjbuS`2)rPNaY7bdcvrJ)=zr;IQCF1%$s*t) zzKA%yd_kx@TFcrkI__yOPZKDB?(?9;?Cj5&jok6-aAC9B+$ir~;no1pX6!3bKVB zS zk>=LFhWu}_K0^XKp+CtM7&zO7cO%^T6r`Z@Zw>F58}cmnoF&Q)9eh%gyf^j1sK4Rh(8Onv)kF{d;vegzs-Na|E5XO z6l+#!+BI7>`!p|T{-ll5Zqy#tzM+fMW$AY5Ucgsj@Q#hXT)#?x+%Va&*l?HOoN=C=$NkcN;OLmmw|A37s+dFY1FjiI-O z4u(Dy`ol4%F>}Va$2>6RTv&YA-mssCeK9s|Y|Gf~W8WP&d0geVL*rf@&&HRGZy$gA z_-Dp{5k4!tKm0`a-|)Ic?Sy{(ot*H8h`fmH5wAqNKe2LR!^E?Z;rJ3(S!8A8JCT2k z3W-XKS{?OZ)c2!?CdEw3n>2e;EMrZ^sf=@(b22w&zMM5F>uAzqe-j6@#WBT2#fyqP#XE{m7e8D4%i_;xPnw-Ed-Loc%%+uL zcqQzYcoMS|h7+-Xe;N@V%btQK&2`v^&yOHi-g9O`Ox|l)7{61w*Rm@9fO4-3D5Gc5 z{3|8hz@jzb%Ds^VX=qOtwI9TiG&7X@V75TBOu09)KFv|(K7?h!kC5d<;hW4rA7zJn6WO` zf|0_Ey-BmgUnW++L1!Pl+%U!6l+D;fV*at=s`JZ z-5U_Ufh|CdE~GetX_mmJ3(!rhA1Pi@qYk+C;PL>jL$qiE)-Hi-HEMPt)r=h$R)mtS zB(!A0Nh%K1Ah=FZz9#^uD%2zFT}W#OK}7e8)JcjHqt&j6-=5Db^`Byyf5pwuM%&vH1J(TI@xE^Y_hwZsACSWrqt_fI00yh zDCtGdFpbdsukR(K<2G14llaSoE&QMVGhMbN3q@TAIOTy=CYwAU(#dWXfyV!N>Ifonw-&bA3P~--`g#@aq_&VW+aYHuZ?n*1 zztz09KGFy*B}mqi=%p6DLUVc$Ix(Kg(TVIUX%1P(PV@j8k*Ob;#4nA-q>FAb{>ssq zdXyYN$wHF$QC&G2Qu#o8??z5HTaVNhKFfz_v^DNq?WA-bTJkFG5Lmi-LKVpx=db;ssg!z`iCil(>^`LHtlh zGC4x1FigNkwGA|SNIyk#lRPi!d-My-)Pi3>ZwP<#h)55q$J3bBfjG5K{*>xhk)%{i zWR7gpEy{X9pY#XF_PN0$zem-r@RYm-vO<2%lA1v;BI_eD z?nOPqiFil+?Zv&cVwBQ_mMPw*q^UHS6dI*WXcTWoP0~XNOLCaJHL5`{2}hEn8kd;L zd@5~5E>TNZbfYx2(=6)Cl3fDNPT^S+FUVV^5#AI)8DUJEZ9#oWhYHapT9RXfQ+UOs zGg8x4NlX19J)}`Wa)+p*F@pEkdYsRZ$X@`jId zs?JF5C5{j;sFzAPP%YwJpwDeWYf^)W>#EgId8QwL~{>m z`=s?5PLH$}gpHixkOl-|LVO?1>o7wLx>1XGtx{$Oer`}T04~UORXV6_3)=PL;~NL9W0dcjA(ca?-T zi4Sd}UB9(Zu^~B;{z>kWT_E{UxhrKE*niC!>B-Ip&R|Vgm6Uy*Xqb*xXr#M7zN;EY zeVz1&Mn38fBtH*k%#xqd_Vl1lvfQe6NJ&b|Z4&%eErzt=-6-!t4T||GBpoJuKonD7 z9PWL|9!cpVrR<+$N(-JOW_E#nHqa+k=gC)-gQ`n`y}U>Ch!$WLXs1agD&>S7*%son zI!c&8;Yf*44Vq6;NmWawTvXg^(8BOFi)Er!b?$yOyjA@nm^5NZZ$i$!Rc*SuR!U!m zMp4pR*^`Mrk}r8VqmM(^(m(Sk`lr&l7?6gKeAh>(DxcB%rPNN!QSC4JJOoAU@!i5w zQVHS^jZu_C_Ea4|dZB~T52CSJEkQmqdC64X3@%Y0kiC@TEmO&ZA>I>oSz5(a_T@kc zUCHUdGA3A(uh#Hr=F&5|Z?ND6aas1BzcF ztsvz{$*CCneLb0{S_GEVOj!G%5xE)?qdX~bb^P$- zVZH(n=?#&7t363lK&=ox1exH;nHkOIiARLTNXtWwRo|z42c)Xs!f?^5cT*hzrB@ zQ}y}?PtvnYgmV5#ZIOlAh~JUET?hINNy3HlWVvW;_OHv4?#mSlvdtu2&t=W@^ME{m z8g=N`iSh(Xj{3w&(qqS{t&`m$YzZQT5;xMvRsl_&Ey|wYr$Ays&uqvalq+I>kCpgL zxQ;gW?nVybCOt+KP9UF&t5l!-U9z~;S805ep7n6wO4?SB(s%L|t3l(|wR-1&r=Lh@ ze%-4BPNVzNCfF`H8xEXZkPk%T8Bw?qvXZ(-Hh{)kIXV-MN#o?WH2MfQe9TjQ7*!jn z7kKbTu%zFrdU~c&=0d}L(JVl25E?^R$~NTKPit^~ui2%@MxArYbui)#@m|G5Y9r~k zsv%_KiCT3QAvMJ$^j7jx9mz+wrAk_zW6lt=CR|CI)b>PWj=|~-U5){g60#nWTe9cUNKCRP>qr_vvpMSVM4iNodZ}F1 zAW6s^vKlnszm~SkRyV?)Ilya@fa(RcV9rYT^#fPxVdR7OB}lqN67hR~QU^xk$F;Rf zttG9aAIj*QrcqqAKWbUI_HeD zQF&=4uQdxI&w^Ia$s1Jt0)j$uKcA(wBTXP_Q%{uE@OoA$2p5&den^2@c=fq!VEaJwIc*FiqfqB$a!tivy1S}UH|5lqr-szFpNMV#)-kyfg}t41w?OHcS{wn?%gOdF8y zhdLaqg(xH8*?@)QOBra8IG3Pp+P^_kStT$BWp-j!wFOWJU!vPD^FVxw^CM+o5_pf)0pcp@6tzXtA%0W| zF4Tj6R^U%9$rPfSw1@P&OyMA>jMJ=b+^FKeo(n83#RM@1se%gtj#7l`GNm+8^+U=6v`d9Yn&}_5~ zmQ~rzjB}z_;~D)Z|9BS?Ikr4&v9nK)_Y|<#4yUe|@J3E3&Rz+_yGG-10!ui~K#9P+ z6OnkIWfIOIipE<)u{aYb9`AIRSt4xS6r8v@4eMAbc#4pUeZ1-5dnW8{HsI!B??67@ zEGobbf+D~8iTdy)N|J%qK17Iqk;&r29PUcmRpUS%({ zSFrQ(V{r3z_8R*Y`yNKfzq4@Ic_&bZmewzJRJU$J-cZFYt|%s#~K^{3gN*q_Bi6 zzKi__dxyQt-e`{K4eV;wSo@bBY6yvk(W_(?4H~TKmq&~q<@^7)9u^+Raupja}`M>idKFId-yVxLv@*DY0e4EbI+~YR&bhmjs z2HYJEm!ZDb-O%41L_vqg;h=bp%Msqv-P!H(dYpDwdvB-R)76cy@U^ekx7uB;c7xTm zq}SeRuy#>kw7Tlu_7;cH+9krVqMcfYM~R2ox_dqT=$LxvMt>~C=4|ivI7G$Z+Fn`sqJ({1YSapT9eUc0G(eXzvd3(Q^3^4`=YnrgN8bd2ff_Bb~Ie|u-M z-7}^_P-^!&yIr+jW0lk8sCT%-YwaF;yT|VCKm@nmZjatY%+d6(*LZq#)?QDy*6H$S zYdX5ZN_#sy9o`zZLzD|`?e=;dtu0PZOQ$2u-T@wM=;&>=cV%{U88&opprA*_I_*x1 zbveBV^lm%-y6D%~(&6l+EUyTR8#=msDcRL7L$8b*`b7iA0l=Wr9lg}LzSo6c9W`xq zcL+#9Aa8&gHna-_jO%5(!6O>bH;cUW5@xG}*{;BBRl;UjB@`q;_&vQuPbetxM?lBr zNQm3+zjyiXLqXByj4?#d<(SFel(E}ocN6|jCA7;hXuu(Ff_u7ql`JJRwRAYt(ym^? zj}S`QfD0{|I(xgE0}kR(h~3@mQtpkL9G-5Z8M<8#3ZOCwg1n^cxG}atJ4NX16Zt*P zjg;Tx=yMR0gB;HG4icWx1-(J2T~?oqD-Tybt{J!ra24Vz!Zj1uEFH?^&`&P?2`%s zZkYpO?6jN2Jyfx`OBAp;?GBH)EBDy@93p$My#=+zZB0A)upxpfH8?w49W{LpPp5q# ztIE@ED)*Gx-4;iu*Dh~sx*Y9xqe!iF23L7q$hP)$7{S||oRS!p2?c(6 zIk^hFTm@dP(r%v8UY^okp3+{Pz&9_aP^nj<6nG#M?I9HSAQbQriuMr-`Vb2G5K4HZWr9A$twb+EfpP|$-=(1TF$ U6QSUz&1#dO%_hs)Y!>#v0hZ1HegFUf literal 0 HcmV?d00001 diff --git a/vendor/mpdf/mpdf/ttfonts/Garuda.ttf b/vendor/mpdf/mpdf/ttfonts/Garuda.ttf index b9fcdfd90e111c6d09bd19bb11d5c283c3ff14e3..0abddd3548c8e595186cea8367e54e3415050191 100644 GIT binary patch delta 4218 zcmZ`+2~oLX}o8vKwO>-PHX@Z#3csOoVl8{xCa1v`xN~}$jKvNp~-^(LFFz=mv|9ijt-T(go z-QNF(&x@}5L|z~Opu|iJih}ZzBCKI$U=K=JQNgl8>6k15>j$vzD6-qiyTYq?V*L&P zmrzt*k@wnjj>Q0PYXGEYZRHD7)^s&LhAIvrzq+=`)%xUsdkny@3i+M2ogSkkS)2(F zP>1cY>sr@)o8EYO^+y2m0R+yiZ``zQ`(r0A0t9CP{PCo_-c?8HE;OOS2y9PxBOpD? zeS>u;Rp4&&Y<%M6?F(2R1&~xXw$!@Ds;(cy0e0blr<+_GTR9IwePko@jm@s6dg;D| zD*&(E0thsRa^@z3&3}cVmaQ+3axpR>M1=SFR1o`UWCJ z=sCa}FVCAp<;^SoW^)qSYHS${dqw;MGsyBp%M?^+N8)waqiJcQSH~yi7dGi+}E=)>JNj0UVFUnXP zza%qjY4*c8xq11x4~vRRN|&20R-3)dQSPj$Tv4@h)g!Bawq|X$>qjn#bN}_(tvg*K z@Xs&co6o5&Ujf|ymKpVr|5EYkmoy(9TfdD;*tn^qv)#jre`EP5)ffnH4{QHnh;LWG z3eMZDIHDL<&R4c7Ur|X_d8(tTA$62GPu;9Oq<&W;)zoOVY6djlYxigmXpd;0)t=Jc z(~jymU7${=JD>5VUcvMbQe{`MKm@rE=t8ez!nC{qw*wHzs;*4>p<|fWP9v>XP zKmOiB=7)L`!V>n+lg-;QUon600@;FHiPFT)iGvH9lj4%jCOeadQc6?4NL`aUXo@mb znrcjYO_$SR)7)t%(!NgDq!*_bFLEzBospjLc*ecOjf;nutX|TcDa-84+@1MFR&|zl zch;Sy5ldT_eweMu-uiIV!$))CbH%yMd4YK^=2zx_U2w4ASi$oJJq6baMweNa^%bfL zR~2q694m?~I$U(OIHEYa*jYSKe7nS0;w;%;GF0+iX?Cfnw7ayg^v?3A<>uu_mw#vW zm@k?K%|qs4^Qc8^QCJ+7HI_#$otABuy<%L&VAOP@8^8fJ~NrdYGBrPfO8PV1-E zyVmb)Qk%*aWm{wGwOzCg+JFfI{Bu!29zWA6+K?L~N6st<15cl|n=v6`TC{;jkc%(`l8Kw(YYL#gcLtf$nxmGRm#l!&k z=6hF2h}*YgNBKPB^u7P5r-DR*aFD-yPUnS}=shErUb2SagyMkuQxFJLt*N5Ocx2N^!$1TJX9o23q=5%Qn~ z-wm*YzjEHdUp=p1nF$tb*@SH=XxMx~ra&@eLLqXS!2|iIp&nTtthXT}8B!q;+2bh% zXE*~9Bc>Es05#BnOegQ^*YZh!5&NoMe_qc2{aO)UaXp`Z`g)9*a74skl=meOF_91{ z@gp*LfyjwJ2_S(ahy;@mq9975B5F7bFGCNUfnURE@bPcSR`IGgTQkTe@;Lc9`~h~r zuke;U2zOv7?1d-bX?PBvC7WS4d!_c`(@*Q;9HZo+Aps9>2{u%lpH=A{TS zRgjBi7}~YEQ%y+|g=E@-Rxu{ZR2tw-#1aHc`2A%M2LX6RO5e-H(3Il-V24H^bmj2w z(Sc}r#Y!U>?YU+nBshz6jbH-6(1!*h$e&r_4}G+Br4ul40YVhW2}CUfqVRg*P`(Pi ztS1NE@LRmK05G-{5{FzB237m+e*}a}`2G^+aWqFf0rM?Iqxc$Nfoo3XlBn z;Td=Vf>1{xCi9d;>pzto*j9yF;_(_Y!4mwO6k=Z-nd344H_Okle2C`c6K*fs!VoTm zU-fb1ia_*;U~lY&dT6kA8Lq%p_$ypPL<&B3d%XgGhU?f4=sn;C33yooWMeA8REo)t z+zbfAHyg2F{=_HqR-zrEn`lQC%TqC*SesHD#+b}JRr!Uf;=h6)8o=B z?iGN3K@JG=LqXmW{hf~4Du){TNpK*ML_B&UO$Zfe}dw@Ee1sSgK{ zoUjKk;mU}{eMN*S<3KbK?0Wt2k4=Iz4{m6v=5X3_mXF@+c zSM)zAl(jx;R28K*{6oyC`L&oO^ARLi!?4yT5FMhZC*#upNn{XFt(Lm>_ilMf?~GkBPi;XJZg!q^!T++*U zK*iYmI(Q{NC=fFRVm6$ZDUdJ)gej0P1!AUvV+uq}0f+r@1EM;5(%5FiI<3(pY*$2N zSeD{EP|i3?`$_ot8*1KiBWy0k#_vUXA9A>AbY7`G`lm`@AItaidv2`7-7CXd4@1r% USAi}yVW8H65#(dOu^S%pfBH+-`~Uy| delta 5623 zcmai23v^S*nf^x-wlTIbwlOw#%s_Y;EM)nKv4Qv@+p@7`S(c5VY=bOaTS_HeRl2q@ zNfBjxb~!0c(x`iuLuof{w=5^!&7rtSC4t|@bf-U<=vcC_~NHvZMfW6eab?<1OXt*fWB{Nc;#3y9bOjPG*? zT%lJV`0)>jlI;*s;~r7?oH9clQFJ z{^=6Eb$SPMH0ZiG1KZz--tk*ZuV;1RcML(@Vx5Wfs=Pun=(5nefjFX4NV{W?Ca;^j zWO^DjPEW-yC08d+Pfx!?6kC>3DMT@p+F1A8q9~9{^d?;ay!Qu#XfIu*cj#UE54uMG zN!RH;xHCJ#w8NM7>M zL-ZB$Q-FdbQ;5DwVcJCx(2^ zv}OXv`sXf6ijRxczj7K*QI$@C!dYw)yN@knOWFNw1zXNmvSPM|t!5?6$V_Z4D`jR@ z&MH|2t77X|HLGK_Yy;cKY;cco-gTJrDdTY14b}r!y$9aOEFTJds48&{&YS)H4h|$1 zUB%Wz>8tQGu~xPBefV{V_QIzImdhGh6YZoi;V;#5H5gaVkoADw%%Q?Ay2U!+O*>*R z50OZN|Kom7XE|&kMrN}DR>&5!24-cO5l=C@9Z^IM5o;U2Fn5bC`gG>Y3vw6qEjYN~ zYL+?6pY=+1YPL1opM4KJCUE2|4{z^{3H1<cy)^ zR$pFYS#z?ayyS?n)_8nv`PyS9v+1zuZs`HD-F&5NcX?{LQjt@!ui|c{wQ^78{>tN( zA640^o~*i1T~xiPI$RxI_vE@mHN3`C^TztV^;5MiwYzFRtlLqi)cs~d`-Z5p;o`=m zjoUW9wDI<)haO0IU|)S!{mq8{hEEy~G}Sj?bWC+z?YP--yK_UQwbR+TqtoBHGuru$&L=t#bsp)wWi!|^ZH2aCTcxeu7P3v+ zF4^9-ePFxOmC|MHI^1=v>vY#d*HqWluABA|`(FEF_Jj7r_G9+b_6hrx{c887?)L7! z?t$)L_pa{0=|17eauhk%II10u4x8hk4DCi>FK+*`nRVehUYWq)4A#I>+(y^jX(9ZEpwP}{MO!{66P51c;>t5(S=Mmv%vdR zMbtc|B&@Oc!q<_3tZM0NkPBb10vEOT&{ue^$d&F!yB*Pd8kh_iYW&pyipT z#qJw5%!Rz8C};p@l*Z<1xQ# zrQsQ5%~Vvph_b0Mlf4t$b@HcCYL$Xg*riAwJ}|~@%}vhcCO#;KxqW!R?{jmD9B}!9 zsi~c^$2aK1sNtYT3iDQ5FOSdkCq{Po+)^+i2{z7-E!cco$%Wl^7<;h`4Oe*B1K5ad zSjLu2jk1h7`niKXm)M6bJ&aAuhxtL`R8A(UqH3yDaWk2zhSs7kLtRGeL8U$^9P!CP zUT&(YuI1%sbIn?F+1j%8=+ZrgxAIdA?`NCD{(~%2O?Y^IxTaX*4TGtZQo`WbUQzG%WM5BCLCoxO4hR(Qb+c8&V{em)@a;fORi>^E{afNyDY zTDy9kyur4WZ)tEi8f?z3br`95W%NoTQk;)35b}$Rf+xeSpfV;j?re56Suwt$v8|)c zxfM!Bc}ttq*4)#>Te=*)f!iA#&bFrBjs^#}_d0M)Y&LP;1F!r)?i4v4ki!!9NQ%qn zkHFHc2wns_`90k08j%o0x8xgvMlSA_L#jTCH~Pgv^2@;?)ilW0(xi_226<3cjC?fg zLpmt3s@q=*osqW%-6kWiDnoQ#!LRt?<{nIL@eL|o-V*i8a@fcl<%l8&jl8pgb8~rF znR#tlg}IFL-kyfk)P@LDj)Ww)&*kURPPe3n*C_N*6ErL##u(y}-NOM1i9G7{xxFH0 zky*8kB5Ao47=Qz2Y~gY=jNEnkkt3joL?PMSckJc?WkBW*`q zejnBeT!4I+)pY`wM1&!u-Z53cjQoIa$O~m>REP@2zc{x{ye;l%ZMn~QaT;2J|v z6bar{duCOIt?F`2ELv?-5PMC0C80OUvC*gwl(8WXsXI01d1UNDGlJ0)E*`~RAsnc7 z8j_aHX>Na;|>Z}KVuKgFhg*@Pn?ZVP#@EH~gUO@~AA3k@@edHTucALr>g zdY*np&(h!Ge|Q`x=WKD%#Bp^I%fr9%KgVj%osQi-m$w*);YaBW`fG}*H;8Z1-_t%i zixd1GV&;kQ=os6@9%heZ4hMba<}$O%BSqYPDH2Ij2^Ezlzci?5jjDSa)%P~msf{?3 z${Lej9(_>E(+KPToX{{sh-yO`XDAw>X@)RQ)g;bU!&QbFuJY1wm5YX}4C2hpmOnSE zETMcqtgf^qTA~p*C2p}0I%9q%q|P=VcD4bzvkeGFV?6iO3#?3IV=;NLL^5_sVR_W& zQM{3m%PpmOWCdpkw=WEbYXjHCiq8N3OK}S-R6lC?Jr-A}7V9m|9m()a%et~A4O)_s zTY_`46i}f!^Wm2-o8HXrT*| zn>)HKG0(5JMt4yXcrd+@@L&OGRe3V~c2zT`AJkx2qyJEYhc)<&29Im-MGa1Ba7u$$ zGaX8QZ$&Q!9oqL)Sy{|wHmZ&(5AtD4Gw6~510l=<cO-%R+tFF&-4&pMd2e4&o#$PPRJwl|~U0#f@4=lNx0nC^*D}@~TEj14YDEhq#Cu z&nlp#tN8g?=(kJ0l;A%f^!X}1!9Np}OqG)0zW@|G4c)_EI7B>j%<|6$C0nJOkKOp~ z(¬t3(P?zkA4keCY!q7OV{g9xAm-zVNbMDOpQPy@jrU(tSTS(1tjYW5rhnTQ-Jx2})LYd` zD#Bbd(n08o#L%s|izTaDDQeK3AaR#UWTGBYGEj0*VAu5H({FvYrhaO{_z`eQMw;Z} zl}q^AM3s1FdRzAxIG>7bzj%K%nZ8MXN8hFI(Lc}+=qH$ye$TVfxcVP;9Gi-W#RWAr zt08j(N+XID#fBmt3%8-{Kp8~wql8g*qU;u$>0+q=_tOML*ypZ zJ7Anxk~wezr 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..1abd3f6f --- /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": "^3.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..b4e5b7d5 --- /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..a0fcb3a5 --- /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/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..879fc6f5 100644 --- a/vendor/psr/log/composer.json +++ b/vendor/psr/log/composer.json @@ -7,20 +7,20 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } } } diff --git a/vendor/psr/log/src/AbstractLogger.php b/vendor/psr/log/src/AbstractLogger.php new file mode 100644 index 00000000..d60a091a --- /dev/null +++ b/vendor/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; } diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/src/LoggerInterface.php similarity index 63% rename from vendor/psr/log/Psr/Log/LoggerInterface.php rename to vendor/psr/log/src/LoggerInterface.php index 2206cfde..cb4cf648 100644 --- a/vendor/psr/log/Psr/Log/LoggerInterface.php +++ b/vendor/psr/log/src/LoggerInterface.php @@ -22,12 +22,9 @@ interface LoggerInterface /** * System is unusable. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function emergency($message, array $context = array()); + public function emergency(string|\Stringable $message, array $context = []): void; /** * Action must be taken immediately. @@ -35,35 +32,26 @@ public function emergency($message, array $context = array()); * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function alert($message, array $context = array()); + public function alert(string|\Stringable $message, array $context = []): void; /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function critical($message, array $context = array()); + public function critical(string|\Stringable $message, array $context = []): void; /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function error($message, array $context = array()); + public function error(string|\Stringable $message, array $context = []): void; /** * Exceptional occurrences that are not errors. @@ -71,55 +59,40 @@ public function error($message, array $context = array()); * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function warning($message, array $context = array()); + public function warning(string|\Stringable $message, array $context = []): void; /** * Normal but significant events. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function notice($message, array $context = array()); + public function notice(string|\Stringable $message, array $context = []): void; /** * Interesting events. * * Example: User logs in, SQL logs. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function info($message, array $context = array()); + public function info(string|\Stringable $message, array $context = []): void; /** * Detailed debug information. * - * @param string $message * @param mixed[] $context - * - * @return void */ - public function debug($message, array $context = array()); + public function debug(string|\Stringable $message, array $context = []): void; /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string $message + * @param mixed $level * @param mixed[] $context * - * @return void - * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, $message, array $context = array()); + public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/vendor/psr/log/Psr/Log/LoggerTrait.php b/vendor/psr/log/src/LoggerTrait.php similarity index 57% rename from vendor/psr/log/Psr/Log/LoggerTrait.php rename to vendor/psr/log/src/LoggerTrait.php index e392fef0..a5d9980b 100644 --- a/vendor/psr/log/Psr/Log/LoggerTrait.php +++ b/vendor/psr/log/src/LoggerTrait.php @@ -14,13 +14,8 @@ trait LoggerTrait { /** * System is unusable. - * - * @param string $message - * @param array $context - * - * @return void */ - public function emergency($message, array $context = array()) + public function emergency(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } @@ -30,13 +25,8 @@ public function emergency($message, array $context = array()) * * 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()) + public function alert(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } @@ -45,13 +35,8 @@ public function alert($message, array $context = array()) * Critical conditions. * * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return void */ - public function critical($message, array $context = array()) + public function critical(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } @@ -59,13 +44,8 @@ public function critical($message, array $context = array()) /** * 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()) + public function error(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } @@ -75,26 +55,16 @@ public function error($message, array $context = array()) * * 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()) + public function warning(string|\Stringable $message, array $context = []): void { $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()) + public function notice(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } @@ -103,26 +73,16 @@ public function notice($message, array $context = array()) * Interesting events. * * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return void */ - public function info($message, array $context = array()) + public function info(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return void */ - public function debug($message, array $context = array()) + public function debug(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } @@ -130,13 +90,9 @@ public function debug($message, array $context = array()) /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void + * @param mixed $level * * @throws \Psr\Log\InvalidArgumentException */ - abstract public function log($level, $message, array $context = array()); + abstract public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/src/NullLogger.php similarity index 74% rename from vendor/psr/log/Psr/Log/NullLogger.php rename to vendor/psr/log/src/NullLogger.php index c8f7293b..de0561e2 100644 --- a/vendor/psr/log/Psr/Log/NullLogger.php +++ b/vendor/psr/log/src/NullLogger.php @@ -15,15 +15,11 @@ class NullLogger extends AbstractLogger /** * Logs with an arbitrary level. * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void + * @param mixed[] $context * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, $message, array $context = array()) + public function log($level, string|\Stringable $message, array $context = []): void { // noop } 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 @@ Date: Mon, 17 Nov 2025 11:02:14 +0100 Subject: [PATCH 02/37] first start at refactoring We now have a few classes that separate the concerns DokuPDF - inherits from mPDF and doesn't do much anymore except for a few internal defaults and overwrites Config - encapsulate the configuration (file and input vars) and created an mpdf config array Template - handles loading the template files and applies placeholders Writer - composites the above classed and gives access to write operations --- DokuPDF.class.php | 96 -------- action.php | 123 ++-------- src/Config.php | 188 +++++++++++++++ .../DokuImageProcessorDecorator.php | 2 +- src/DokuPDF.php | 63 +++++ src/Styles.php | 10 + src/Template.php | 176 ++++++++++++++ src/Writer.php | 215 ++++++++++++++++++ 8 files changed, 678 insertions(+), 195 deletions(-) delete mode 100644 DokuPDF.class.php create mode 100644 src/Config.php rename DokuImageProcessorDecorator.php => src/DokuImageProcessorDecorator.php (99%) create mode 100644 src/DokuPDF.php create mode 100644 src/Styles.php create mode 100644 src/Template.php create mode 100644 src/Writer.php diff --git a/DokuPDF.class.php b/DokuPDF.class.php deleted file mode 100644 index 41a4f34d..00000000 --- a/DokuPDF.class.php +++ /dev/null @@ -1,96 +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/action.php b/action.php index 1b665733..13f756aa 100644 --- a/action.php +++ b/action.php @@ -5,7 +5,12 @@ use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; use dokuwiki\plugin\dw2pdf\MenuItem; +use dokuwiki\plugin\dw2pdf\src\Config; +use dokuwiki\plugin\dw2pdf\src\DokuPdf; +use dokuwiki\plugin\dw2pdf\src\Template; +use dokuwiki\plugin\dw2pdf\src\Writer; use dokuwiki\StyleUtils; +use Mpdf\HTMLParserMode; use Mpdf\MpdfException; /** @@ -423,20 +428,13 @@ protected function generatePDF($cachefile, $event) } //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"); + $config = new Config($this->conf, $this->getDocumentLanguage($this->list[0])); + $mpdf = new DokuPDF($config); + $template = new Template($this->getConf('template'), $this->getConf('qrcodescale')); + $writer = new Writer($mpdf, $template); - $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); @@ -446,64 +444,12 @@ protected function generatePDF($cachefile, $event) } $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 .= ''; - } + $writer->startDocument($this->title); + $writer->cover(); - $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; - } + // FIXME where to move this? 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 @@ -517,43 +463,24 @@ protected function generatePDF($cachefile, $event) 'outdent' => '1em', 'pagenumstyle' => '1' ]); - $html .= ''; + + $mpdf->WriteHTML('', HTMLParserMode::HTML_BODY, false, false); } // loop over all pages $counter = 0; - $no_pages = count($this->list); foreach ($this->list as $page) { + $template->setContext($page, $this->title, $rev, $date_at, $INPUT->server->str('REMOTE_USER', '', true)); + $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; - } + $writer->wikiPage($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 .= ''; - } + $writer->back(); + $writer->endDocument(); //Return html for debugging if ($isDebug) { @@ -616,12 +543,12 @@ protected function loadTemplate() // this is what we'll return $output = [ - 'cover' => '', - 'back' => '', - 'html' => '', - 'page' => '', - 'first' => '', - 'cite' => '', + 'cover' => '', // cover page + 'back' => '', // back page + 'html' => '', // header/footer html sections + 'page' => '', // pseudo CSS to register header/footers + 'first' => '', // pseudo CSS to register first page header/footers + 'cite' => '', // citation box html ]; // prepare header/footer elements diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..00510c7f --- /dev/null +++ b/src/Config.php @@ -0,0 +1,188 @@ +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->lang = $lang; + $this->loadPluginConfig($pluginConf); + $this->loadInputConfig(); + } + + /** + * Apply the given configuration + * + * @param array $conf Plugin configuration + */ + public function loadPluginConfig(array $conf) + { + if (isset($conf['pagesize'])) $this->pagesize = $conf['pagesize']; + if (isset($conf['orientation'])) $this->isLandscape = ($conf['orientation'] === 'landscape'); + if (isset($conf['font-size'])) $this->fontSize = (int)$conf['font-size']; + if (isset($conf['doublesided'])) $this->isDoublesided = (bool)$conf['doublesided']; + if (isset($conf['toc'])) $this->hasToC = (bool)$conf['toc']; + if (isset($conf['toclevels'])) $this->tocLevels = $this->parseTocLevels($conf['toclevels']); + if (isset($conf['maxbookmarks'])) $this->maxBookmarks = (int)$conf['maxbookmarks']; + if (isset($conf['template'])) $this->template = $conf['template']; + } + + /** + * Load configuration provided by INPUT parameters + * + * Not all parameters are overridable here + * + * @return void + */ + public function loadInputConfig() + { + global $INPUT; + $this->pagesize = $INPUT->str('pagesize', $this->pagesize); + if ($INPUT->has('orientation')) { + $this->isLandscape = $INPUT->str('orientation') === 'landscape'; + } + $this->fontSize = $INPUT->int('font-size', $this->fontSize); + $this->isDoublesided = $INPUT->bool('doublesided', $this->isDoublesided); + if ($INPUT->has('toclevels')) { + $this->tocLevels = $this->parseTocLevels($INPUT->str('toclevels')); + } + $this->watermark = $INPUT->str('watermark', $this->watermark); + } + + + /** + * 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; + } + + + /** + * Get the mode to use + * @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 + * @return string + */ + public function getMode(): string + { + switch ($this->lang) { + case 'zh': + case 'zh-tw': + case 'ja': + case 'ko': + return '+aCJK'; + default: + return 'UTF-8-s'; + } + } + + /** + * Return the paper format + * + * @return string + */ + public function getFormat() + { + $format = $this->pagesize; + if ($this->isLandscape) { + $format .= '-L'; + } + return $format; + } + + /** + * Return the writing direction based on the set language + * + * @return string + */ + public function getDirectionality() + { + switch ($this->lang) { + case 'ar': + case 'he': + return 'rtl'; + default: + return 'ltr'; + } + } + + public function getWatermarkText() + { + return $this->watermark; + } + + /** + * Get all configuration for mpdf as array + * + * Note: wrtiting direction needs to be set separately + * + * @link https://mpdf.github.io/reference/mpdf-variables/overview.html + * @return array + */ + public function getMPdfConfig(): array + { + return [ + 'mode' => $this->getMode(), + '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, + ]; + } +} diff --git a/DokuImageProcessorDecorator.php b/src/DokuImageProcessorDecorator.php similarity index 99% rename from DokuImageProcessorDecorator.php rename to src/DokuImageProcessorDecorator.php index d5aea140..2eae3bdd 100644 --- a/DokuImageProcessorDecorator.php +++ b/src/DokuImageProcessorDecorator.php @@ -1,6 +1,6 @@ + */ +class DokuPdf extends Mpdf +{ + /** + * DokuPDF constructor. + * + * @param Config $config + * @throws MpdfException + * @throws \Exception + */ + public function __construct(Config $config) + { + + // FIXME this needs to be passed differently + // 'ImageProcessorClass' => DokuImageProcessorDecorator::class, + // either by monkeypatching the property to protected or via reflection + + parent::__construct($config->getMPdfConfig()); + $this->SetDirectionality($config->getDirectionality()); + + // 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()); + } + + /** + * Cleanup temp dir + */ + public function __destruct() + { + // FIXME do we still need to clean up ourselves? + } + + /** + * 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/src/Styles.php b/src/Styles.php new file mode 100644 index 00000000..64bfd221 --- /dev/null +++ b/src/Styles.php @@ -0,0 +1,10 @@ + '', + 'rev' => '', + 'at' => '', + 'title' => '', + 'username' => '', + ]; + + + /** + * @param string $name The name of the template to use + * @param float $qrScale The scale of the QR code to generate (0.0 to disable) + */ + public function __construct(string $name = 'default', float $qrScale = 0.0) + { + $this->name = $name; + $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(string $id, string $title, ?string $rev, ?string $at, ?string $username): void + { + $this->context = [ + 'title' => $title, + 'id' => $id, + 'rev' => $rev ?? '', + 'at' => $at ?? '', + '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()) { + $content = 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*(.*?))?\)@/', + [$this, 'replaceDate'], + $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..77788a63 --- /dev/null +++ b/src/Writer.php @@ -0,0 +1,215 @@ +mpdf = $mpdf; + $this->template = $template; + } + + /** + * Initialize the document + * + * @param string $title + * @return void + * @throws MpdfException + */ + public function startDocument(string $title): void + { + $this->mpdf->SetTitle($title); + + // Set the styles FIXME to be moved into Styles class + $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 }'; + // FIXME$styles .= $this->loadCSS(); + $this->mpdf->WriteHTML($styles, HTMLParserMode::HEADER_CSS); + + //start body html + $this->mpdf->WriteHTML('
', HTMLParserMode::HTML_BODY, true, false); + } + + /** + * Insert a page break + * + * @return void + * @throws MpdfException + */ + public function pageBreak(): void + { + $this->mpdf->WriteHTML('', 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->mpdf->WriteHTML($html, HTMLParserMode::HTML_BODY, false, false); + + // add citation box if any + $cite = $this->template->getHTML('citation'); + if ($cite) { + $this->mpdf->WriteHTML($cite, HTMLParserMode::HTML_BODY, false, false); + } + + $this->breakAfterMe(); + } + + public function toc(): void + { + $this->conditionalPageBreak(); + // FIXME + $this->breakAfterMe(); + } + + /** + * 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 + { + $this->conditionalPageBreak(); + + $html = $this->template->getHTML('cover'); + if (!$html) return; + + $this->mpdf->WriteHTML($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 + { + $this->conditionalPageBreak(); + + $html = $this->template->getHTML('back'); + if (!$html) return; + + $this->mpdf->WriteHTML($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->mpdf->WriteHTML('
', 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 + */ + 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; + } +} From b883aece6d4ddb595b067030f097d4977388443e Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 17 Nov 2025 11:27:20 +0100 Subject: [PATCH 03/37] add toc handling --- action.php | 19 ++----------------- src/Config.php | 9 +++++++++ src/Writer.php | 27 +++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/action.php b/action.php index 13f756aa..2f42c8e0 100644 --- a/action.php +++ b/action.php @@ -448,23 +448,8 @@ protected function generatePDF($cachefile, $event) $writer->startDocument($this->title); $writer->cover(); - - // FIXME where to move this? - 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' - ]); - - $mpdf->WriteHTML('', HTMLParserMode::HTML_BODY, false, false); + if($config->hasToC()) { + $writer->toc($this->getLang('tocheader')); } // loop over all pages diff --git a/src/Config.php b/src/Config.php index 00510c7f..a0f8e527 100644 --- a/src/Config.php +++ b/src/Config.php @@ -74,6 +74,15 @@ public function loadInputConfig() $this->watermark = $INPUT->str('watermark', $this->watermark); } + /** + * Check whether ToC is enabled + * + * @return bool + */ + public function hasToc() + { + return $this->hasToC; + } /** * Parses the ToC levels configuration into an array diff --git a/src/Writer.php b/src/Writer.php index 77788a63..5092a081 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -83,11 +83,30 @@ public function wikiPage(string $html): void $this->breakAfterMe(); } - public function toc(): void + /** + * 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->conditionalPageBreak(); - // FIXME - $this->breakAfterMe(); + $this->mpdf->TOCpagebreakByArray([ + 'toc-preHTML' => '

' . $header . '

', + 'toc-bookmarkText' => $header, + 'links' => true, + 'outdent' => '1em', + 'pagenumstyle' => '1' + ]); + + $this->mpdf->WriteHTML('', HTMLParserMode::HTML_BODY, false, false); } /** From 1d334a8c8f109c02c2dfca5856a1b9d05c5776e3 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 17 Nov 2025 11:46:30 +0100 Subject: [PATCH 04/37] handle debug writing --- action.php | 23 +++---------- src/Config.php | 12 +++++++ src/DokuPDF.php | 8 +++++ src/Writer.php | 87 +++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/action.php b/action.php index 2f42c8e0..5e33dfb4 100644 --- a/action.php +++ b/action.php @@ -427,23 +427,10 @@ protected function generatePDF($cachefile, $event) $date_at = ''; } - //some shortcuts to export settings - $isDebug = $this->getExportConfig('isDebug'); - $config = new Config($this->conf, $this->getDocumentLanguage($this->list[0])); $mpdf = new DokuPDF($config); $template = new Template($this->getConf('template'), $this->getConf('qrcodescale')); - $writer = new Writer($mpdf, $template); - - - // 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); - + $writer = new Writer($mpdf, $template, $config->isDebugEnabled()); $writer->startDocument($this->title); $writer->cover(); @@ -468,11 +455,9 @@ protected function generatePDF($cachefile, $event) $writer->endDocument(); //Return html for debugging - if ($isDebug) { - if ($INPUT->str('debughtml', 'text', true) == 'text') { - header('Content-Type: text/plain; charset=utf-8'); - } - echo $html; + if ($config->isDebugEnabled()) { + header('Content-Type: text/html; charset=utf-8'); + echo $writer->getDebugHTML(); exit(); } diff --git a/src/Config.php b/src/Config.php index a0f8e527..810b6d9e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -16,6 +16,7 @@ class Config protected string $watermark = ''; protected string $template = 'default'; protected string $lang = 'en'; + protected bool $isDebug = false; /** * @param array $pluginConf Plugin configuration @@ -72,6 +73,7 @@ public function loadInputConfig() $this->tocLevels = $this->parseTocLevels($INPUT->str('toclevels')); } $this->watermark = $INPUT->str('watermark', $this->watermark); + $this->isDebug = $INPUT->bool('debug', $this->isDebug); } /** @@ -84,6 +86,16 @@ public function hasToc() return $this->hasToC; } + /** + * Check whether debug mode is enabled + * + * @return bool + */ + public function isDebugEnabled () + { + return $this->isDebug; + } + /** * Parses the ToC levels configuration into an array * diff --git a/src/DokuPDF.php b/src/DokuPDF.php index e333aa55..df165d7e 100644 --- a/src/DokuPDF.php +++ b/src/DokuPDF.php @@ -39,6 +39,14 @@ public function __construct(Config $config) $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); } /** diff --git a/src/Writer.php b/src/Writer.php index 5092a081..160527a8 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -2,27 +2,37 @@ namespace dokuwiki\plugin\dw2pdf\src; +use dokuwiki\ErrorHandler; use Mpdf\HTMLParserMode; use Mpdf\MpdfException; -/** - * @todo handle actual writing in a separate method to allow for centralized error handling and debug writing to HTML - */ class Writer { - + /** @var DokuPdf Our MPDF instance */ protected DokuPdf $mpdf; + + /** @var Template The template used */ protected Template $template; + + /** @var bool Signal to output a page break before the next output */ protected bool $breakBeforeNext = false; + /** @var bool Are we debugging? */ + protected bool $debug = false; + + /** @var string Store HTML when debugging */ + protected string $debugHTML = ''; + /** * @param DokuPdf $mpdf * @param Template $template + * @param bool $debug */ - public function __construct(DokuPdf $mpdf, Template $template) + public function __construct(DokuPdf $mpdf, Template $template, bool $debug = false) { $this->mpdf = $mpdf; $this->template = $template; + $this->debug = $debug; } /** @@ -42,10 +52,10 @@ public function startDocument(string $title): void $styles .= '@page portrait-page { size:portrait }'; $styles .= 'div.dw2pdf-portrait { page:portrait-page }'; // FIXME$styles .= $this->loadCSS(); - $this->mpdf->WriteHTML($styles, HTMLParserMode::HEADER_CSS); + $this->write($styles, HTMLParserMode::HEADER_CSS); //start body html - $this->mpdf->WriteHTML('
', HTMLParserMode::HTML_BODY, true, false); + $this->write('
', HTMLParserMode::HTML_BODY, true, false); } /** @@ -56,7 +66,7 @@ public function startDocument(string $title): void */ public function pageBreak(): void { - $this->mpdf->WriteHTML('', 2, false, false); + $this->write('', 2, false, false); } /** @@ -72,12 +82,12 @@ public function wikiPage(string $html): void $this->applyHeaderFooters(); - $this->mpdf->WriteHTML($html, HTMLParserMode::HTML_BODY, false, false); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); // add citation box if any $cite = $this->template->getHTML('citation'); if ($cite) { - $this->mpdf->WriteHTML($cite, HTMLParserMode::HTML_BODY, false, false); + $this->write($cite, HTMLParserMode::HTML_BODY, false, false); } $this->breakAfterMe(); @@ -106,7 +116,7 @@ public function toc(string $header): void 'pagenumstyle' => '1' ]); - $this->mpdf->WriteHTML('', HTMLParserMode::HTML_BODY, false, false); + $this->write('', HTMLParserMode::HTML_BODY, false, false); } /** @@ -125,7 +135,7 @@ public function cover(): void $html = $this->template->getHTML('cover'); if (!$html) return; - $this->mpdf->WriteHTML($html, HTMLParserMode::HTML_BODY, false, false); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); $this->breakAfterMe(); } @@ -146,7 +156,7 @@ public function back(): void $html = $this->template->getHTML('back'); if (!$html) return; - $this->mpdf->WriteHTML($html, HTMLParserMode::HTML_BODY, false, false); + $this->write($html, HTMLParserMode::HTML_BODY, false, false); } /** @@ -158,7 +168,7 @@ public function back(): void public function endDocument(): void { // adds the closing div and finalizes the document - $this->mpdf->WriteHTML('
', HTMLParserMode::HTML_BODY, false, true); + $this->write('
', HTMLParserMode::HTML_BODY, false, true); } /** @@ -213,6 +223,7 @@ protected function applyHeaderFooters(): void * Insert a page break if there was previous content * * @return void + * @throws MpdfException */ protected function conditionalPageBreak(): void { @@ -231,4 +242,52 @@ 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; + } + + /** + * 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"; + } + } } From 8c4b1418c9a2a4f3ed4dbd8bb494fb439addf4cd Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 17 Nov 2025 11:50:03 +0100 Subject: [PATCH 05/37] remove a few now unused methods --- action.php | 166 ----------------------------------------------------- 1 file changed, 166 deletions(-) diff --git a/action.php b/action.php index 5e33dfb4..4e1bdb83 100644 --- a/action.php +++ b/action.php @@ -500,172 +500,6 @@ protected function sendPDFFile($cachefile) 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' => '', // cover page - 'back' => '', // back page - 'html' => '', // header/footer html sections - 'page' => '', // pseudo CSS to register header/footers - 'first' => '', // pseudo CSS to register first page header/footers - 'cite' => '', // citation box html - ]; - - // 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; - } - - - /** - * (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 * From 081fc334dcf0291898ab7fba8f551bc2b0a7d48a Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 17 Nov 2025 13:46:47 +0100 Subject: [PATCH 06/37] Handle styles We now have a Styles class that takes care of loading the styles from plugins etc. --- action.php | 105 ++--------------------------------------- src/Config.php | 31 ++++++++++++- src/Styles.php | 123 ++++++++++++++++++++++++++++++++++++++++++++++++- src/Writer.php | 10 ++-- 4 files changed, 161 insertions(+), 108 deletions(-) diff --git a/action.php b/action.php index 4e1bdb83..063ab8b0 100644 --- a/action.php +++ b/action.php @@ -7,6 +7,7 @@ use dokuwiki\plugin\dw2pdf\MenuItem; use dokuwiki\plugin\dw2pdf\src\Config; use dokuwiki\plugin\dw2pdf\src\DokuPdf; +use dokuwiki\plugin\dw2pdf\src\Styles; use dokuwiki\plugin\dw2pdf\src\Template; use dokuwiki\plugin\dw2pdf\src\Writer; use dokuwiki\StyleUtils; @@ -429,8 +430,9 @@ protected function generatePDF($cachefile, $event) $config = new Config($this->conf, $this->getDocumentLanguage($this->list[0])); $mpdf = new DokuPDF($config); + $styles = new Styles($config); $template = new Template($this->getConf('template'), $this->getConf('qrcodescale')); - $writer = new Writer($mpdf, $template, $config->isDebugEnabled()); + $writer = new Writer($mpdf, $template, $styles, $config->isDebugEnabled()); $writer->startDocument($this->title); $writer->cover(); @@ -500,107 +502,6 @@ protected function sendPDFFile($cachefile) exit(); } - /** - * 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'); - - // 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/', - ] - ); - $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); - } - - /** - * 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 * diff --git a/src/Config.php b/src/Config.php index 810b6d9e..7b1f844d 100644 --- a/src/Config.php +++ b/src/Config.php @@ -17,6 +17,7 @@ class Config protected string $template = 'default'; protected string $lang = 'en'; protected bool $isDebug = false; + protected array $useStyles = []; /** * @param array $pluginConf Plugin configuration @@ -51,6 +52,11 @@ public function loadPluginConfig(array $conf) if (isset($conf['toclevels'])) $this->tocLevels = $this->parseTocLevels($conf['toclevels']); if (isset($conf['maxbookmarks'])) $this->maxBookmarks = (int)$conf['maxbookmarks']; if (isset($conf['template'])) $this->template = $conf['template']; + if (isset($conf['usestyles'])) { + $this->useStyles = explode(',', $conf['usestyles']); + $this->useStyles = array_map('trim', $this->useStyles); + $this->useStyles = array_filter($this->useStyles); + } } /** @@ -81,7 +87,7 @@ public function loadInputConfig() * * @return bool */ - public function hasToc() + public function hasToc(): bool { return $this->hasToC; } @@ -91,11 +97,32 @@ public function hasToc() * * @return bool */ - public function isDebugEnabled () + 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; + } + + /** * Parses the ToC levels configuration into an array * diff --git a/src/Styles.php b/src/Styles.php index 64bfd221..beb523b4 100644 --- a/src/Styles.php +++ b/src/Styles.php @@ -2,9 +2,130 @@ namespace dokuwiki\plugin\dw2pdf\src; -class Styles { +use dokuwiki\StyleUtils; +class Styles +{ + protected Config $config; + + /** + * @param Config $config + */ + public function __construct(Config $config) + { + $this->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 + 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/Writer.php b/src/Writer.php index 160527a8..7ea4f743 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -14,6 +14,9 @@ class Writer /** @var Template The template used */ protected Template $template; + /** @var Styles The style parser */ + protected Styles $styles; + /** @var bool Signal to output a page break before the next output */ protected bool $breakBeforeNext = false; @@ -28,10 +31,11 @@ class Writer * @param Template $template * @param bool $debug */ - public function __construct(DokuPdf $mpdf, Template $template, bool $debug = false) + public function __construct(DokuPdf $mpdf, Template $template, Styles $styles, bool $debug = false) { $this->mpdf = $mpdf; $this->template = $template; + $this->styles = $styles; $this->debug = $debug; } @@ -46,12 +50,12 @@ public function startDocument(string $title): void { $this->mpdf->SetTitle($title); - // Set the styles FIXME to be moved into Styles class + // 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 }'; - // FIXME$styles .= $this->loadCSS(); + $styles .= $this->styles->getCSS(); $this->write($styles, HTMLParserMode::HEADER_CSS); //start body html From 2bef96e630c886cfaf37188a992bf21369f07ac0 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 20 Nov 2025 13:45:50 +0100 Subject: [PATCH 07/37] move page collecting into separate classes --- action.php | 145 +-------------- src/AbstractCollector.php | 67 +++++++ src/BookCreatorLiveSelectionCollector.php | 20 +++ src/BookCreatorSavedSelectionCollector.php | 27 +++ src/CollectorFactory.php | 38 ++++ src/Exception.php | 8 + src/NamespaceCollector.php | 200 +++++++++++++++++++++ src/PageCollector.php | 21 +++ 8 files changed, 386 insertions(+), 140 deletions(-) create mode 100644 src/AbstractCollector.php create mode 100644 src/BookCreatorLiveSelectionCollector.php create mode 100644 src/BookCreatorSavedSelectionCollector.php create mode 100644 src/CollectorFactory.php create mode 100644 src/Exception.php create mode 100644 src/NamespaceCollector.php create mode 100644 src/PageCollector.php diff --git a/action.php b/action.php index 063ab8b0..def419f3 100644 --- a/action.php +++ b/action.php @@ -5,6 +5,7 @@ use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; use dokuwiki\plugin\dw2pdf\MenuItem; +use dokuwiki\plugin\dw2pdf\src\CollectorFactory; use dokuwiki\plugin\dw2pdf\src\Config; use dokuwiki\plugin\dw2pdf\src\DokuPdf; use dokuwiki\plugin\dw2pdf\src\Styles; @@ -152,148 +153,12 @@ public function convert(Event $event) */ protected function collectExportablePages(Event $event) { - global $ID, $REV; + global $REV, $DATE_AT; 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')); - } + $collector = CollectorFactory::create($event->data, $REV, $DATE_AT); + $list = $collector->getPages(); + $title = $collector->getTitle(); $list = array_map('cleanID', $list); diff --git a/src/AbstractCollector.php b/src/AbstractCollector.php new file mode 100644 index 00000000..d53848e5 --- /dev/null +++ b/src/AbstractCollector.php @@ -0,0 +1,67 @@ +rev = $rev; + $this->at = $at; + $this->title = $INPUT->str('book_title'); + $this->pages = $this->collect(); + } + + /** + * Collect the pages to be included in the PDF + * + * @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 list of page ids to include in the PDF + * + * @return string[] + */ + public function getPages(): array + { + return $this->pages; + } +} diff --git a/src/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php new file mode 100644 index 00000000..878b468a --- /dev/null +++ b/src/BookCreatorLiveSelectionCollector.php @@ -0,0 +1,20 @@ +str('selection', '', true); + $list = json_decode($selection, true, 512, JSON_THROW_ON_ERROR); + return (array) $list; + } +} diff --git a/src/BookCreatorSavedSelectionCollector.php b/src/BookCreatorSavedSelectionCollector.php new file mode 100644 index 00000000..7cf8da8e --- /dev/null +++ b/src/BookCreatorSavedSelectionCollector.php @@ -0,0 +1,27 @@ +loadSavedSelection($INPUT->str('savedselection')); + if(!$this->title && !empty($savedselection['title'])) { + $this->title = $savedselection['title']; + } + + return (array) $savedselection['selection']; + } +} diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php new file mode 100644 index 00000000..7e2f08da --- /dev/null +++ b/src/CollectorFactory.php @@ -0,0 +1,38 @@ +has('selection') ) { + return new BookCreatorLiveSelectionCollector($rev, $at); + } elseif($INPUT->has('savedselection')) { + return new BookCreatorSavedSelectionCollector($rev, $at); + } + // fallthrough + default: + throw new \InvalidArgumentException('Invalid export configuration'); + } + } + +} diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 00000000..1f7136e8 --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,8 @@ +namespace = cleanID($INPUT->str('book_ns')); + $this->sortorder = $INPUT->str('book_order', 'natural', true); + $this->depth = $INPUT->int('book_nsdepth', 0); + if ($this->depth < 0) $this->depth = 0; + $this->excludePages = array_map('cleanID', $INPUT->arr('excludes')); + $this->excludeNamespaces = array_map('cleanID', $INPUT->arr('excludesns')); + + // 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, function ($page) { + return !in_array($page['id'], $this->excludePages); + }); + $pages = array_filter($pages, function ($page) { + foreach ($this->excludeNamespaces as $ns) { + if (strpos($page['id'], $ns . ':') === 0) { + 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..88a8ea50 --- /dev/null +++ b/src/PageCollector.php @@ -0,0 +1,21 @@ +rev)) { + return []; + } + + return [$ID]; + } + +} From ac65d8f6a69f474180b1d21daca3431ad3b930ed Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 20 Nov 2025 13:50:18 +0100 Subject: [PATCH 08/37] remove some moved methods --- action.php | 69 ------------------------------------------------------ 1 file changed, 69 deletions(-) diff --git a/action.php b/action.php index def419f3..1549d6e7 100644 --- a/action.php +++ b/action.php @@ -377,75 +377,6 @@ 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: From 6afd3369e3c4b9247eeee9dee04a3c9c254f00cb Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 20 Nov 2025 13:53:45 +0100 Subject: [PATCH 09/37] remove obsolete template event --- action.php | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/action.php b/action.php index 1549d6e7..20060935 100644 --- a/action.php +++ b/action.php @@ -79,7 +79,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'); } @@ -476,36 +475,6 @@ public function getExportConfig($name, $notset = false) 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 * From 293d84e688578c409a79ab630af525484f49e065 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Mon, 24 Nov 2025 14:33:23 +0100 Subject: [PATCH 10/37] more refactoring The basic handling of page collecting, caching and rendering is now defined. There is still some ugly dependency in the renderer and the ImageProcessor loading is not reimplemented yet. After the above a first attempt to actually run the code can be made. --- action.php | 239 +++++--------------------------------- src/AbstractCollector.php | 64 +++++++++- src/Cache.php | 89 ++++++++++++++ src/CollectorFactory.php | 8 +- src/Config.php | 73 ++++++------ src/DokuPDF.php | 52 ++++++++- src/Template.php | 18 +-- src/Writer.php | 35 ++++++ 8 files changed, 309 insertions(+), 269 deletions(-) create mode 100644 src/Cache.php diff --git a/action.php b/action.php index 20060935..733a23ad 100644 --- a/action.php +++ b/action.php @@ -1,18 +1,17 @@ */ - 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); - } - } + protected $currentBookChapter = 0; /** * Return the value of currentBookChapter, which is the order of the file to be added in a book generation @@ -90,7 +56,6 @@ public function register(EventHandler $controller) public function convert(Event $event) { global $REV, $DATE_AT; - global $conf, $INPUT; // our event? $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; @@ -98,145 +63,31 @@ public function convert(Event $event) 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 $REV, $DATE_AT; - global $INPUT; - + $this->loadConfig(); + $config = new Config($this->conf); $collector = CollectorFactory::create($event->data, $REV, $DATE_AT); - $list = $collector->getPages(); - $title = $collector->getTitle(); + $cache = new Cache($config, $collector); + - $list = array_map('cleanID', $list); + if (!$cache->useCache()) { + // generating the pdf may take a long time for larger wikis / namespaces with many pages + set_time_limit(0); - $skippedpages = []; - foreach ($list as $index => $pageid) { - if (auth_quickaclcheck($pageid) < AUTH_READ) { - $skippedpages[] = $pageid; - unset($list[$index]); + try { + $this->generatePDF($config, $collector, $cache->cache, $event); + } catch (Exception $e) { + // FIXME should we log here? + // FIXME there was special handling for BookCreator with $INPUT->has('selection') before + nice_die($e->getMessage()); } } - $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)); - } + $event->preventDefault(); // after prevent, $event->data cannot be changed - return [$title, $list]; + // deliver the file + $this->sendPDFFile($cache->cache); //exits } - /** - * 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 @@ -279,41 +130,28 @@ protected function wikiToDW2PDF($id, $rev = '', $date_at = '') * @param Event $event * @throws MpdfException */ - protected function generatePDF($cachefile, $event) + protected function generatePDF(Config $config, AbstractCollector $collector, $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 = ''; - } - - $config = new Config($this->conf, $this->getDocumentLanguage($this->list[0])); - $mpdf = new DokuPDF($config); + $mpdf = new DokuPDF($config, $collector->getLanguage()); $styles = new Styles($config); - $template = new Template($this->getConf('template'), $this->getConf('qrcodescale')); + $template = new Template($config); $writer = new Writer($mpdf, $template, $styles, $config->isDebugEnabled()); - $writer->startDocument($this->title); + $writer->startDocument($collector->getTitle()); $writer->cover(); - if($config->hasToC()) { + if ($config->hasToC()) { $writer->toc($this->getLang('tocheader')); } // loop over all pages $counter = 0; - foreach ($this->list as $page) { - $template->setContext($page, $this->title, $rev, $date_at, $INPUT->server->str('REMOTE_USER', '', true)); - - $this->currentBookChapter = $counter; - $counter++; - $pagehtml = $this->wikiToDW2PDF($page, $rev, $date_at); - $writer->wikiPage($pagehtml); + foreach ($collector->getPages() as $page) { + $template->setContext($collector, $page, $INPUT->server->str('REMOTE_USER', '', true)); + $this->currentBookChapter = $counter++; //FIXME I don't like this + $writer->renderWikiPage($collector, $page); } // insert the back page @@ -494,26 +332,5 @@ 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/src/AbstractCollector.php b/src/AbstractCollector.php index d53848e5..a86e270b 100644 --- a/src/AbstractCollector.php +++ b/src/AbstractCollector.php @@ -27,7 +27,12 @@ public function __construct(?int $rev = null, ?int $at = null) $this->rev = $rev; $this->at = $at; $this->title = $INPUT->str('book_title'); - $this->pages = $this->collect(); + + // 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 + ); } /** @@ -55,6 +60,50 @@ public function getTitle(): string 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 * @@ -64,4 +113,17 @@ 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/Cache.php b/src/Cache.php new file mode 100644 index 00000000..b620b8cd --- /dev/null +++ b/src/Cache.php @@ -0,0 +1,89 @@ +collector = $collector; + + $key = join(':', [ + join(',', sort($collector->getPages())), + $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->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'); + } + + // 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 index 7e2f08da..fedbdba7 100644 --- a/src/CollectorFactory.php +++ b/src/CollectorFactory.php @@ -20,14 +20,14 @@ static public function create(string $event, ?int $rev, ?int $at) switch ($event) { case 'export_page': - return new PageCollector($rev, $at); + return new PageCollector($rev); case 'export_pdfns': - return new NamespaceCollector($rev, $at); + return new NamespaceCollector(null, $at); // $at would make sense but is not supported yet case 'export_pdfbook': if( $INPUT->has('selection') ) { - return new BookCreatorLiveSelectionCollector($rev, $at); + return new BookCreatorLiveSelectionCollector(); } elseif($INPUT->has('savedselection')) { - return new BookCreatorSavedSelectionCollector($rev, $at); + return new BookCreatorSavedSelectionCollector(); } // fallthrough default: diff --git a/src/Config.php b/src/Config.php index 7b1f844d..4e252adb 100644 --- a/src/Config.php +++ b/src/Config.php @@ -15,15 +15,14 @@ class Config protected int $maxBookmarks = 5; protected string $watermark = ''; protected string $template = 'default'; - protected string $lang = 'en'; protected bool $isDebug = false; protected array $useStyles = []; + protected float $qrCodeScale = 0.0; /** * @param array $pluginConf Plugin configuration - * @param string $lang Document language */ - public function __construct(array $pluginConf = [], string $lang = 'en') + public function __construct(array $pluginConf = []) { global $conf; $this->tempDir = $conf['tmpdir'] . '/mpdf'; @@ -32,7 +31,6 @@ public function __construct(array $pluginConf = [], string $lang = 'en') // set default ToC levels from main config $this->tocLevels = $this->parseTocLevels($conf['toptoclevel'] . '-' . $conf['maxtoclevel']); - $this->lang = $lang; $this->loadPluginConfig($pluginConf); $this->loadInputConfig(); } @@ -57,6 +55,8 @@ public function loadPluginConfig(array $conf) $this->useStyles = array_map('trim', $this->useStyles); $this->useStyles = array_filter($this->useStyles); } + if (isset($conf['watermark'])) $this->watermark = $conf['watermark']; // FIXME currently not in default config + if (isset($conf['qrcodescale'])) $this->qrCodeScale = (float)$conf['qrcodescale']; } /** @@ -122,6 +122,33 @@ public function getTemplateName(): string return $this->template; } + /** + * Get the QR code scale + * + * @return float + */ + public function getQRScale(): float + { + return $this->qrCodeScale; + } + + /** + * Get a unique cache key for the current configuration + * + * @return string + */ + public function getCacheKey(): string + { + return join(',', [ + $this->template, + $this->pagesize, + $this->isLandscape ? 'L' : 'P', + $this->fontSize, + $this->isDoublesided ? 'D' : 'S', + $this->hasToC ? 'T' : 'N', + implode('-', $this->tocLevels) + ]); + } /** * Parses the ToC levels configuration into an array @@ -148,26 +175,6 @@ protected function parseTocLevels(string $toclevels): array } - /** - * Get the mode to use - * @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 - * @return string - */ - public function getMode(): string - { - switch ($this->lang) { - case 'zh': - case 'zh-tw': - case 'ja': - case 'ko': - return '+aCJK'; - default: - return 'UTF-8-s'; - } - } - /** * Return the paper format * @@ -183,22 +190,11 @@ public function getFormat() } /** - * Return the writing direction based on the set language + * Return the watermark text if any * * @return string */ - public function getDirectionality() - { - switch ($this->lang) { - case 'ar': - case 'he': - return 'rtl'; - default: - return 'ltr'; - } - } - - public function getWatermarkText() + public function getWatermarkText(): string { return $this->watermark; } @@ -206,7 +202,7 @@ public function getWatermarkText() /** * Get all configuration for mpdf as array * - * Note: wrtiting direction needs to be set separately + * Note: mode and wrtiting direction are set in DokuPDF based on the language * * @link https://mpdf.github.io/reference/mpdf-variables/overview.html * @return array @@ -214,7 +210,6 @@ public function getWatermarkText() public function getMPdfConfig(): array { return [ - 'mode' => $this->getMode(), 'format' => $this->getFormat(), 'default_font_size' => $this->fontSize, 'tempDir' => $this->tempDir, diff --git a/src/DokuPDF.php b/src/DokuPDF.php index df165d7e..4a0a4d4b 100644 --- a/src/DokuPDF.php +++ b/src/DokuPDF.php @@ -21,18 +21,20 @@ class DokuPdf extends Mpdf * DokuPDF constructor. * * @param Config $config + * @param string $lang The language code to use for this document * @throws MpdfException - * @throws \Exception */ - public function __construct(Config $config) + public function __construct(Config $config, string $lang) { // FIXME this needs to be passed differently // 'ImageProcessorClass' => DokuImageProcessorDecorator::class, // either by monkeypatching the property to protected or via reflection - parent::__construct($config->getMPdfConfig()); - $this->SetDirectionality($config->getDirectionality()); + $initConfig = $config->getMPdfConfig(); + $initConfig['mode'] = $this->lang2mode($lang); + parent::__construct($initConfig); + $this->SetDirectionality($this->lang2direction($lang)); // configure page numbering // https://mpdf.github.io/paging/page-numbering.html @@ -60,12 +62,50 @@ public function __destruct() /** * Decode all paths, since DokuWiki uses XHTML compliant URLs * - * @param string $path - * @param string $basepath + * @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/Template.php b/src/Template.php index fda868eb..f7df587d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -32,12 +32,14 @@ class Template /** - * @param string $name The name of the template to use - * @param float $qrScale The scale of the QR code to generate (0.0 to disable) + * Constructor + * + * @param Config $config The DW2PDF configuration */ - public function __construct(string $name = 'default', float $qrScale = 0.0) + public function __construct(Config $config) { - $this->name = $name; + $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"); @@ -56,13 +58,13 @@ public function __construct(string $name = 'default', float $qrScale = 0.0) * @param string|null $username The username of the user generating the PDF (if any) * @return void */ - public function setContext(string $id, string $title, ?string $rev, ?string $at, ?string $username): void + public function setContext(AbstractCollector $collector, string $id, ?string $username): void { $this->context = [ - 'title' => $title, + 'title' => $collector->getTitle(), 'id' => $id, - 'rev' => $rev ?? '', - 'at' => $at ?? '', + 'rev' => $collector->getRev() ?? '', + 'at' => $collector->getAt() ?? '', 'username' => $username ?? '', ]; } diff --git a/src/Writer.php b/src/Writer.php index 7ea4f743..c075e652 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -97,6 +97,41 @@ public function wikiPage(string $html): void $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 + $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $pageId, $rev)), $info, $at); + } else { + $ret = p_cached_output($file, 'dw2pdf', $pageId); + } + + //restore ID (just in case) + $ID = $keep; + + $this->wikiPage($ret); + } + /** * Write the Table of Contents * From 0eefd8f75e7d5fffec5526227d4a00751e770800 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 25 Nov 2025 11:14:34 +0100 Subject: [PATCH 11/37] decouple renderer This starts to approach #528 and #526 --- action.php | 4 ++-- renderer.php | 41 ++++++++++++----------------------------- src/Config.php | 24 ++++++++++++++++++++++++ src/Writer.php | 37 +++++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/action.php b/action.php index 733a23ad..9e5a3b15 100644 --- a/action.php +++ b/action.php @@ -132,12 +132,12 @@ protected function wikiToDW2PDF($id, $rev = '', $date_at = '') */ protected function generatePDF(Config $config, AbstractCollector $collector, $cachefile, $event) { - global $REV, $INPUT, $DATE_AT; + global $INPUT; $mpdf = new DokuPDF($config, $collector->getLanguage()); $styles = new Styles($config); $template = new Template($config); - $writer = new Writer($mpdf, $template, $styles, $config->isDebugEnabled()); + $writer = new Writer($mpdf, $config, $template, $styles, $config->isDebugEnabled()); $writer->startDocument($collector->getTitle()); $writer->cover(); diff --git a/renderer.php b/renderer.php index 8315c864..b165b722 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) @@ -18,35 +20,22 @@ class renderer_plugin_dw2pdf extends Doku_Renderer_xhtml private static $header_count = []; private static $previous_level = 0; - /** - * Stores action instance - * - * @var action_plugin_dw2pdf - */ - private $actioninstance; - - /** - * load action plugin instance - */ - public function __construct() - { - $this->actioninstance = plugin_load('action', 'dw2pdf'); - } - public function document_start() { global $ID; 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(); + static $chapter = 0; // FIXME we can probably do without a static here and use a class property + $chapter++; + self::$header_count[1] = $chapter; } /** @@ -89,7 +78,7 @@ public function header($text, $level, $pos, $returnonly = false) // retrieve numbered headings option - $isnumberedheadings = $this->actioninstance->getExportConfig('headernumber'); + $isnumberedheadings = $this->getConf('headernumber'); $header_prefix = ""; if ($isnumberedheadings) { @@ -110,7 +99,7 @@ public function header($text, $level, $pos, $returnonly = false) // add PDF bookmark $bookmark = ''; - $maxbookmarklevel = $this->actioninstance->getExportConfig('maxbookmarks'); + $maxbookmarklevel = $this->getConf('maxbookmarks'); // 0: off, 1-6: show down to this level if ($maxbookmarklevel && $maxbookmarklevel >= $level) { $bookmarklevel = $this->calculateBookmarklevel($level); @@ -238,21 +227,15 @@ 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; - } - // prefix interwiki links with interwiki icon if ($link['name'][0] != '<' && preg_match('/\binterwiki iw_(.\w+)\b/', $link['class'], $m)) { if (file_exists(DOKU_INC . 'lib/images/interwiki/' . $m[1] . '.png')) { diff --git a/src/Config.php b/src/Config.php index 4e252adb..bd4d973e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -13,6 +13,7 @@ class Config protected bool $hasToC = false; protected array $tocLevels = []; protected int $maxBookmarks = 5; + protected bool $numberedHeaders = false; protected string $watermark = ''; protected string $template = 'default'; protected bool $isDebug = false; @@ -49,6 +50,7 @@ public function loadPluginConfig(array $conf) if (isset($conf['toc'])) $this->hasToC = (bool)$conf['toc']; if (isset($conf['toclevels'])) $this->tocLevels = $this->parseTocLevels($conf['toclevels']); if (isset($conf['maxbookmarks'])) $this->maxBookmarks = (int)$conf['maxbookmarks']; + if (isset($conf['headernumber'])) $this->numberedHeaders = (bool)$conf['headernumber']; if (isset($conf['template'])) $this->template = $conf['template']; if (isset($conf['usestyles'])) { $this->useStyles = explode(',', $conf['usestyles']); @@ -122,6 +124,26 @@ 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 * @@ -146,6 +168,8 @@ public function getCacheKey(): string $this->fontSize, $this->isDoublesided ? 'D' : 'S', $this->hasToC ? 'T' : 'N', + $this->maxBookmarks, + $this->numberedHeaders ? 'H' : 'N', implode('-', $this->tocLevels) ]); } diff --git a/src/Writer.php b/src/Writer.php index c075e652..8b6e722e 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -11,6 +11,9 @@ class Writer /** @var DokuPdf Our MPDF instance */ protected DokuPdf $mpdf; + /** @var Config The configuration */ + protected Config $config; + /** @var Template The template used */ protected Template $template; @@ -31,9 +34,10 @@ class Writer * @param Template $template * @param bool $debug */ - public function __construct(DokuPdf $mpdf, Template $template, Styles $styles, bool $debug = false) + public function __construct(DokuPdf $mpdf, Config $config, Template $template, Styles $styles, bool $debug = false) { $this->mpdf = $mpdf; + $this->config = $config; $this->template = $template; $this->styles = $styles; $this->debug = $debug; @@ -121,15 +125,40 @@ public function renderWikiPage(AbstractCollector $collector, string $pageId): vo if ($collector->getRev()) { //no caching on old revisions - $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $pageId, $rev)), $info, $at); + $html = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $pageId, $rev)), $info, $at); } else { - $ret = p_cached_output($file, 'dw2pdf', $pageId); + $html = p_cached_output($file, 'dw2pdf', $pageId); } //restore ID (just in case) $ID = $keep; - $this->wikiPage($ret); + // 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. + * + * @todo implement + * @param AbstractCollector $collector + * @param string $html The rendered HTML of the wiki page + * @return string + */ + protected function fixInternalLinks(AbstractCollector $collector, string $html): string + { + +// // 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; +// } + return $html; } /** From ab12f14dd349572112b58c3ec1267994b9ae104e Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 25 Nov 2025 11:19:58 +0100 Subject: [PATCH 12/37] removed more obsolete methods from action plugin --- action.php | 153 ----------------------------------------------------- 1 file changed, 153 deletions(-) diff --git a/action.php b/action.php index 9e5a3b15..0f9b469e 100644 --- a/action.php +++ b/action.php @@ -27,16 +27,6 @@ class action_plugin_dw2pdf extends ActionPlugin { - protected $currentBookChapter = 0; - - /** - * 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 * @@ -88,41 +78,6 @@ public function convert(Event $event) $this->sendPDFFile($cache->cache); //exits } - - /** - * 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 * @@ -204,114 +159,6 @@ protected function sendPDFFile($cachefile) exit(); } - /** - * Returns array of pages which will be included in the exported pdf - * - * @return array - */ - public function getExportedPages() - { - return $this->list; - } - - - /** - * 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 page tools, new SVG based mechanism From f77f381a704166e8cb938d236b6840daf207e91a Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 10:21:56 +0100 Subject: [PATCH 13/37] fixed issues from first real run --- action.php | 21 +++++++++------------ src/Cache.php | 6 ++++-- src/CollectorFactory.php | 2 +- src/{DokuPDF.php => DokuPdf.php} | 0 src/Template.php | 11 +++++++++-- src/Writer.php | 3 +++ 6 files changed, 26 insertions(+), 17 deletions(-) rename src/{DokuPDF.php => DokuPdf.php} (100%) diff --git a/action.php b/action.php index 0f9b469e..a2425650 100644 --- a/action.php +++ b/action.php @@ -55,27 +55,23 @@ public function convert(Event $event) $this->loadConfig(); $config = new Config($this->conf); - $collector = CollectorFactory::create($event->data, $REV, $DATE_AT); + $collector = CollectorFactory::create($event->data, ((int) $REV) ?: null, ((int) $DATE_AT) ?: null); $cache = new Cache($config, $collector); - if (!$cache->useCache()) { + if (!$cache->useCache() || $config->isDebugEnabled()) { // generating the pdf may take a long time for larger wikis / namespaces with many pages set_time_limit(0); - try { - $this->generatePDF($config, $collector, $cache->cache, $event); - } catch (Exception $e) { - // FIXME should we log here? - // FIXME there was special handling for BookCreator with $INPUT->has('selection') before - nice_die($e->getMessage()); - } + // Exceptions bubble up and should be handled by DokuWiki + $this->generatePDF($config, $collector, $cache->cache, $event); + // FIXME there was special handling for BookCreator with $INPUT->has('selection') before } $event->preventDefault(); // after prevent, $event->data cannot be changed // deliver the file - $this->sendPDFFile($cache->cache); //exits + $this->sendPDFFile($cache->cache, $collector->getTitle()); //exits } /** @@ -126,8 +122,9 @@ protected function generatePDF(Config $config, AbstractCollector $collector, $ca /** * @param string $cachefile + * @param string $title */ - protected function sendPDFFile($cachefile) + protected function sendPDFFile(string $cachefile, string $title) { header('Content-Type: application/pdf'); header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); @@ -136,7 +133,7 @@ protected function sendPDFFile($cachefile) global $INPUT; $outputTarget = $INPUT->str('outputTarget', $this->getConf('output')); - $filename = rawurlencode(cleanID(strtr($this->title, ':/;"', ' '))); + $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); if ($outputTarget === 'file') { header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); } else { diff --git a/src/Cache.php b/src/Cache.php index b620b8cd..341d8cc6 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -12,8 +12,10 @@ public function __construct(Config $config, AbstractCollector $collector) { $this->collector = $collector; + $pages = $collector->getPages(); + sort($pages); $key = join(':', [ - join(',', sort($collector->getPages())), + join(',', $pages), $config->getCacheKey(), $collector->getTitle(), ]); @@ -55,7 +57,7 @@ protected function addDependencies() // images and included pages $dependencies = []; - foreach ($this->list as $pageid) { + foreach ($this->collector->getPages() as $pageid) { $relations = p_get_metadata($pageid, 'relation'); if (is_array($relations)) { diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php index fedbdba7..e94a6843 100644 --- a/src/CollectorFactory.php +++ b/src/CollectorFactory.php @@ -19,7 +19,7 @@ static public function create(string $event, ?int $rev, ?int $at) global $INPUT; switch ($event) { - case 'export_page': + case 'export_pdf': return new PageCollector($rev); case 'export_pdfns': return new NamespaceCollector(null, $at); // $at would make sense but is not supported yet diff --git a/src/DokuPDF.php b/src/DokuPdf.php similarity index 100% rename from src/DokuPDF.php rename to src/DokuPdf.php diff --git a/src/Template.php b/src/Template.php index f7df587d..831dcb3c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -143,7 +143,7 @@ protected function replacePlaceholders(string $html): string ]; $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata); if ($event->advise_before()) { - $content = str_replace(array_keys($replace), array_values($replace), $html); + $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(); @@ -151,7 +151,14 @@ protected function replacePlaceholders(string $html): string // @DATE([, ])@ $html = preg_replace_callback( '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', - [$this, 'replaceDate'], + 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 ); diff --git a/src/Writer.php b/src/Writer.php index 8b6e722e..ea96c13f 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -29,6 +29,9 @@ class Writer /** @var string Store HTML when debugging */ protected string $debugHTML = ''; + /** @var false Is this the first page being written? */ + protected bool $isFirstPage = true; + /** * @param DokuPdf $mpdf * @param Template $template From decfd8d656c558b3e1f93dc2bbc580387c1accd2 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 11:53:36 +0100 Subject: [PATCH 14/37] extract $INPUT dependencies out of Collector classes This should make tests and a future command line interface easier to implement. --- action.php | 4 +- src/AbstractCollector.php | 18 ++- src/BookCreatorLiveSelectionCollector.php | 5 +- src/BookCreatorSavedSelectionCollector.php | 7 +- src/CollectorFactory.php | 17 ++- src/Config.php | 123 +++++++++++++++++++++ src/NamespaceCollector.php | 12 +- 7 files changed, 159 insertions(+), 27 deletions(-) diff --git a/action.php b/action.php index a2425650..8ba2d8cc 100644 --- a/action.php +++ b/action.php @@ -45,7 +45,7 @@ public function register(EventHandler $controller) */ public function convert(Event $event) { - global $REV, $DATE_AT; + global $REV, $DATE_AT, $INPUT; // our event? $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; @@ -55,7 +55,7 @@ public function convert(Event $event) $this->loadConfig(); $config = new Config($this->conf); - $collector = CollectorFactory::create($event->data, ((int) $REV) ?: null, ((int) $DATE_AT) ?: null); + $collector = CollectorFactory::create($event->data, $config, ((int) $REV) ?: null, ((int) $DATE_AT) ?: null); $cache = new Cache($config, $collector); diff --git a/src/AbstractCollector.php b/src/AbstractCollector.php index a86e270b..cba06e95 100644 --- a/src/AbstractCollector.php +++ b/src/AbstractCollector.php @@ -16,17 +16,17 @@ abstract class AbstractCollector * @var int|null */ protected ?int $at; + protected Config $config; /** * Constructor */ - public function __construct(?int $rev = null, ?int $at = null) + public function __construct(Config $config, ?int $rev = null, ?int $at = null) { - global $INPUT; - + $this->config = $config; $this->rev = $rev; $this->at = $at; - $this->title = $INPUT->str('book_title'); + $this->title = $config->getBookTitle() ?? ''; // collected pages are cleaned and checked for read access $this->pages = array_filter( @@ -35,6 +35,16 @@ public function __construct(?int $rev = null, ?int $at = null) ); } + /** + * 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 * diff --git a/src/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php index 878b468a..f8686fe5 100644 --- a/src/BookCreatorLiveSelectionCollector.php +++ b/src/BookCreatorLiveSelectionCollector.php @@ -11,9 +11,8 @@ class BookCreatorLiveSelectionCollector extends AbstractCollector */ protected function collect(): array { - global $INPUT; - - $selection = $INPUT->str('selection', '', true); + $selection = $this->getConfig()->getLiveSelection(); + if ($selection === null) return []; $list = json_decode($selection, true, 512, JSON_THROW_ON_ERROR); return (array) $list; } diff --git a/src/BookCreatorSavedSelectionCollector.php b/src/BookCreatorSavedSelectionCollector.php index 7cf8da8e..77f1ea1a 100644 --- a/src/BookCreatorSavedSelectionCollector.php +++ b/src/BookCreatorSavedSelectionCollector.php @@ -11,13 +11,14 @@ class BookCreatorSavedSelectionCollector extends AbstractCollector */ protected function collect(): array { - global $INPUT; - /** @var action_plugin_bookcreator_handleselection $bcPlugin */ $bcPlugin = plugin_load('action', 'bookcreator_handleselection'); if( !$bcPlugin ) return []; - $savedselection = $bcPlugin->loadSavedSelection($INPUT->str('savedselection')); + $savedSelectionId = $this->getConfig()->getSavedSelection(); + if ($savedSelectionId === null) return []; + + $savedselection = $bcPlugin->loadSavedSelection($savedSelectionId); if(!$this->title && !empty($savedselection['title'])) { $this->title = $savedselection['title']; } diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php index e94a6843..af7358ab 100644 --- a/src/CollectorFactory.php +++ b/src/CollectorFactory.php @@ -9,25 +9,24 @@ class CollectorFactory * Returns the appropriate collector for the given export event * * @param string $event The name of the export event + * @param Config $config Combined plugin and request configuration * @param int|null $rev A specific revision to export * @param int|null $at A specific dateat timestamp to export * @throws \InvalidArgumentException If the event is not recognized * @return AbstractCollector */ - static public function create(string $event, ?int $rev, ?int $at) + static public function create(string $event, Config $config, ?int $rev, ?int $at) { - global $INPUT; - switch ($event) { case 'export_pdf': - return new PageCollector($rev); + return new PageCollector($config, $rev, $at); case 'export_pdfns': - return new NamespaceCollector(null, $at); // $at would make sense but is not supported yet + return new NamespaceCollector($config, $rev, $at); // $at would make sense but is not supported yet case 'export_pdfbook': - if( $INPUT->has('selection') ) { - return new BookCreatorLiveSelectionCollector(); - } elseif($INPUT->has('savedselection')) { - return new BookCreatorSavedSelectionCollector(); + if( $config->hasLiveSelection() ) { + return new BookCreatorLiveSelectionCollector($config, $rev, $at); + } elseif($config->hasSavedSelection()) { + return new BookCreatorSavedSelectionCollector($config, $rev, $at); } // fallthrough default: diff --git a/src/Config.php b/src/Config.php index bd4d973e..d443bdd7 100644 --- a/src/Config.php +++ b/src/Config.php @@ -20,6 +20,16 @@ class Config protected array $useStyles = []; protected float $qrCodeScale = 0.0; + // Collector-specific request data + protected ?string $bookTitle = null; + protected string $bookNamespace = ''; + protected string $bookSortOrder = 'natural'; + protected int $bookNamespaceDepth = 0; + protected array $bookExcludePages = []; + protected array $bookExcludeNamespaces = []; + protected ?string $liveSelection = null; + protected ?string $savedSelection = null; + /** * @param array $pluginConf Plugin configuration */ @@ -82,6 +92,19 @@ public function loadInputConfig() } $this->watermark = $INPUT->str('watermark', $this->watermark); $this->isDebug = $INPUT->bool('debug', $this->isDebug); + + $this->bookTitle = $INPUT->str('book_title') ?: null; + $this->bookNamespace = cleanID($INPUT->str('book_ns')); + $this->bookSortOrder = $INPUT->str('book_order', $this->bookSortOrder, true); + $this->bookNamespaceDepth = max(0, $INPUT->int('book_nsdepth', $this->bookNamespaceDepth)); + $this->bookExcludePages = array_map('cleanID', $INPUT->arr('excludes')); + $this->bookExcludeNamespaces = array_map('cleanID', $INPUT->arr('excludesns')); + + $selection = $INPUT->has('selection') ? $INPUT->str('selection', '', true) : null; + $this->liveSelection = ($selection !== null && $selection !== '') ? $selection : null; + + $saved = $INPUT->has('savedselection') ? $INPUT->str('savedselection') : null; + $this->savedSelection = ($saved !== null && $saved !== '') ? $saved : null; } /** @@ -252,4 +275,104 @@ public function getMPdfConfig(): array '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; + } } diff --git a/src/NamespaceCollector.php b/src/NamespaceCollector.php index 6a6c554a..8a1c119e 100644 --- a/src/NamespaceCollector.php +++ b/src/NamespaceCollector.php @@ -21,14 +21,14 @@ class NamespaceCollector extends AbstractCollector */ protected function initVars(): void { - global $INPUT; + $config = $this->getConfig(); - $this->namespace = cleanID($INPUT->str('book_ns')); - $this->sortorder = $INPUT->str('book_order', 'natural', true); - $this->depth = $INPUT->int('book_nsdepth', 0); + $this->namespace = $config->getBookNamespace(); + $this->sortorder = $config->getBookSortOrder(); + $this->depth = $config->getBookNamespaceDepth(); if ($this->depth < 0) $this->depth = 0; - $this->excludePages = array_map('cleanID', $INPUT->arr('excludes')); - $this->excludeNamespaces = array_map('cleanID', $INPUT->arr('excludesns')); + $this->excludePages = $config->getBookExcludedPages(); + $this->excludeNamespaces = $config->getBookExcludedNamespaces(); // check namespace exists $nsdir = dirname(wikiFN($this->namespace . ':dummy')); From 4c3a95e330df64bfa98daeb7847e8e8eb7b4cd91 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 12:36:59 +0100 Subject: [PATCH 15/37] reimplement ImageProcessor differently As suggested in mpdf/mpdf#2145 the previous decorator functionality was implemented via custom HTTPClient and LocalContent loaders. --- src/DokuImageProcessorDecorator.php | 113 ----------------- src/DokuPdf.php | 14 ++- src/HttpClient.php | 91 ++++++++++++++ src/LocalContentLoader.php | 32 +++++ src/MediaLinkResolver.php | 188 ++++++++++++++++++++++++++++ 5 files changed, 319 insertions(+), 119 deletions(-) delete mode 100644 src/DokuImageProcessorDecorator.php create mode 100644 src/HttpClient.php create mode 100644 src/LocalContentLoader.php create mode 100644 src/MediaLinkResolver.php diff --git a/src/DokuImageProcessorDecorator.php b/src/DokuImageProcessorDecorator.php deleted file mode 100644 index 2eae3bdd..00000000 --- a/src/DokuImageProcessorDecorator.php +++ /dev/null @@ -1,113 +0,0 @@ - DokuImageProcessorDecorator::class, - // either by monkeypatching the property to protected or via reflection - $initConfig = $config->getMPdfConfig(); $initConfig['mode'] = $this->lang2mode($lang); - parent::__construct($initConfig); + + $container = new SimpleContainer([ + 'httpClient' => new HttpClient(), + 'localContentLoader' => new LocalContentLoader(), + ]); + + parent::__construct($initConfig, $container); $this->SetDirectionality($this->lang2direction($lang)); // configure page numbering diff --git a/src/HttpClient.php b/src/HttpClient.php new file mode 100644 index 00000000..efdd83c8 --- /dev/null +++ b/src/HttpClient.php @@ -0,0 +1,91 @@ +getUri(); + if ($uri === null) { + return new Response(); + } + + $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()); + $success = $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..78d165bc --- /dev/null +++ b/src/LocalContentLoader.php @@ -0,0 +1,32 @@ +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..ca7e1dd1 --- /dev/null +++ b/src/MediaLinkResolver.php @@ -0,0 +1,188 @@ + 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(strpos($mime, 'image/') === 0) { + $localFile = $this->resizedMedia($localFile, $ext, $w, $h); + } + } else { + [, $mime] = mimetype($file); + if (strpos($mime, 'image/') !== 0) 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 (substr($file, 0, 9) === '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; + } +} From 38ca8b27367d65a81856f5b4e17baec34a844f56 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 15:11:16 +0100 Subject: [PATCH 16/37] updated tests for Media resolving --- _test/DokuImageProcessorTest.php | 73 ------------------ _test/MediaLinkResolverTest.php | 124 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 73 deletions(-) delete mode 100644 _test/DokuImageProcessorTest.php create mode 100644 _test/MediaLinkResolverTest.php 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/MediaLinkResolverTest.php b/_test/MediaLinkResolverTest.php new file mode 100644 index 00000000..4c0d4652 --- /dev/null +++ b/_test/MediaLinkResolverTest.php @@ -0,0 +1,124 @@ +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']); + } + + 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); + + $this->assertNotNull($resolved); + $this->assertFileExists($resolved['path']); + $this->assertSame('image/gif', $resolved['mime']); + $this->assertSame(2523, filesize($resolved['path'])); + } + + public function testResolveRejectsNonImages(): void + { + $resolved = $this->resolver->resolve(DOKU_URL . 'README'); + + $this->assertNull($resolved); + } + + 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']); + } +} + From c617724c859ebe81d339c98993c3c072a3c76b16 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 15:13:59 +0100 Subject: [PATCH 17/37] update general test --- _test/GeneralTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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'])); } From 8f1c137a0431448a6f9eda59dddfd7a309b68cbf Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 15:17:14 +0100 Subject: [PATCH 18/37] removed unneeded custom exception --- src/Exception.php | 8 -------- src/NamespaceCollector.php | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 src/Exception.php diff --git a/src/Exception.php b/src/Exception.php deleted file mode 100644 index 1f7136e8..00000000 --- a/src/Exception.php +++ /dev/null @@ -1,8 +0,0 @@ -namespace . ':dummy')); - if (!@is_dir($nsdir)) throw new Exception('needns'); + if (!@is_dir($nsdir)) throw new \Exception('needns'); } /** @@ -46,7 +46,7 @@ protected function collect(): array try { $this->initVars(); - } catch (Exception $e) { + } catch (\Exception $e) { return []; } From bbcdb3fed72404e038f1746aa5568749e162edc7 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 16:43:33 +0100 Subject: [PATCH 19/37] rewrite internal links. implements #526 --- composer.json | 2 ++ renderer.php | 16 ++++++++++ src/Writer.php | 81 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 61e4e4c8..3f58eba4 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,8 @@ } }, "require": { + "ext-dom": "*", + "ext-libxml": "*", "mpdf/mpdf": "8.2.*", "mpdf/qrcode": "^1.2" }, diff --git a/renderer.php b/renderer.php index b165b722..c5352f1d 100644 --- a/renderer.php +++ b/renderer.php @@ -236,6 +236,22 @@ public function acronym($acronym) */ public function _formatLink($link) { + // 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 if ($link['name'][0] != '<' && preg_match('/\binterwiki iw_(.\w+)\b/', $link['class'], $m)) { if (file_exists(DOKU_INC . 'lib/images/interwiki/' . $m[1] . '.png')) { diff --git a/src/Writer.php b/src/Writer.php index ea96c13f..aa6fc14f 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -2,6 +2,7 @@ namespace dokuwiki\plugin\dw2pdf\src; +use DOMDocument; use dokuwiki\ErrorHandler; use Mpdf\HTMLParserMode; use Mpdf\MpdfException; @@ -145,23 +146,83 @@ public function renderWikiPage(AbstractCollector $collector, string $pageId): vo * 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. * - * @todo implement * @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; -// // 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; -// } - 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; } /** From 45269358351d3e0e0490a434fbfcbd3ded39542a Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 16:53:31 +0100 Subject: [PATCH 20/37] address some of the remaining FIXMEs --- conf/default.php | 1 + conf/metadata.php | 1 + lang/en/settings.php | 1 + src/Config.php | 2 +- src/DokuPdf.php | 8 -------- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/conf/default.php b/conf/default.php index 77aae8d1..c088360c 100644 --- a/conf/default.php +++ b/conf/default.php @@ -8,6 +8,7 @@ $conf['toclevels'] = ''; $conf['headernumber'] = 0; $conf['maxbookmarks'] = 5; +$conf['watermark'] = ''; $conf['template'] = 'default'; $conf['output'] = 'file'; $conf['usecache'] = 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/src/Config.php b/src/Config.php index d443bdd7..754e0f2c 100644 --- a/src/Config.php +++ b/src/Config.php @@ -67,7 +67,7 @@ public function loadPluginConfig(array $conf) $this->useStyles = array_map('trim', $this->useStyles); $this->useStyles = array_filter($this->useStyles); } - if (isset($conf['watermark'])) $this->watermark = $conf['watermark']; // FIXME currently not in default config + if (isset($conf['watermark'])) $this->watermark = $conf['watermark']; if (isset($conf['qrcodescale'])) $this->qrCodeScale = (float)$conf['qrcodescale']; } diff --git a/src/DokuPdf.php b/src/DokuPdf.php index c2b233c3..d8a826a4 100644 --- a/src/DokuPdf.php +++ b/src/DokuPdf.php @@ -53,14 +53,6 @@ public function __construct(Config $config, string $lang) $this->SetBasePath($url); } - /** - * Cleanup temp dir - */ - public function __destruct() - { - // FIXME do we still need to clean up ourselves? - } - /** * Decode all paths, since DokuWiki uses XHTML compliant URLs * From 5eabda9a042d4a83ceb53ef418ad4cf58350c257 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 16:53:54 +0100 Subject: [PATCH 21/37] fix additional blank page at the end --- src/Writer.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Writer.php b/src/Writer.php index aa6fc14f..37b8f84e 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -283,11 +283,10 @@ public function cover(): void */ public function back(): void { - $this->conditionalPageBreak(); - $html = $this->template->getHTML('back'); if (!$html) return; + $this->conditionalPageBreak(); $this->write($html, HTMLParserMode::HTML_BODY, false, false); } From b706c939b1e720778e501178ded83cdf61845564 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 17:19:48 +0100 Subject: [PATCH 22/37] fixed remaining sort test --- _test/ActionPagenameSortTest.php | 104 --------------------------- _test/NamespaceCollectorSortTest.php | 95 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 104 deletions(-) delete mode 100644 _test/ActionPagenameSortTest.php create mode 100644 _test/NamespaceCollectorSortTest.php 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/NamespaceCollectorSortTest.php b/_test/NamespaceCollectorSortTest.php new file mode 100644 index 00000000..569571cd --- /dev/null +++ b/_test/NamespaceCollectorSortTest.php @@ -0,0 +1,95 @@ + [[ + '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 array $expected + * @return void + */ + 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); + } +} + From 32d393ae187f2aea965a2d7917e9059a39b3e7fe Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 18:53:34 +0100 Subject: [PATCH 23/37] have collectors check for page existance --- src/AbstractCollector.php | 4 ++++ src/BookCreatorLiveSelectionCollector.php | 4 ++-- src/BookCreatorSavedSelectionCollector.php | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/AbstractCollector.php b/src/AbstractCollector.php index cba06e95..9c5c293b 100644 --- a/src/AbstractCollector.php +++ b/src/AbstractCollector.php @@ -48,6 +48,10 @@ protected function getConfig(): 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; diff --git a/src/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php index f8686fe5..2b67015f 100644 --- a/src/BookCreatorLiveSelectionCollector.php +++ b/src/BookCreatorLiveSelectionCollector.php @@ -13,7 +13,7 @@ protected function collect(): array { $selection = $this->getConfig()->getLiveSelection(); if ($selection === null) return []; - $list = json_decode($selection, true, 512, JSON_THROW_ON_ERROR); - return (array) $list; + $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 index 77f1ea1a..ec716709 100644 --- a/src/BookCreatorSavedSelectionCollector.php +++ b/src/BookCreatorSavedSelectionCollector.php @@ -23,6 +23,7 @@ protected function collect(): array $this->title = $savedselection['title']; } - return (array) $savedselection['selection']; + $list = (array) $savedselection['selection']; + return array_filter($list, fn($page) => page_exists($page)); } } From b23d7b8a47aa2f6dcd762e603c809d617715599f Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 18:53:57 +0100 Subject: [PATCH 24/37] for exporting single pages do not rely on $ID We want the ID to be passed via Config as well. --- src/Config.php | 16 +++++++++++++++- src/PageCollector.php | 9 ++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Config.php b/src/Config.php index 754e0f2c..b4235749 100644 --- a/src/Config.php +++ b/src/Config.php @@ -29,6 +29,7 @@ class Config protected array $bookExcludeNamespaces = []; protected ?string $liveSelection = null; protected ?string $savedSelection = null; + protected string $exportId = ''; /** * @param array $pluginConf Plugin configuration @@ -80,7 +81,7 @@ public function loadPluginConfig(array $conf) */ public function loadInputConfig() { - global $INPUT; + global $INPUT, $ID; $this->pagesize = $INPUT->str('pagesize', $this->pagesize); if ($INPUT->has('orientation')) { $this->isLandscape = $INPUT->str('orientation') === 'landscape'; @@ -105,6 +106,9 @@ public function loadInputConfig() $saved = $INPUT->has('savedselection') ? $INPUT->str('savedselection') : null; $this->savedSelection = ($saved !== null && $saved !== '') ? $saved : null; + + $requestID = $INPUT->str('id', $ID ?? '', true); + $this->exportId = cleanID($requestID); } /** @@ -375,4 +379,14 @@ 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/PageCollector.php b/src/PageCollector.php index 88a8ea50..7fa9ab5e 100644 --- a/src/PageCollector.php +++ b/src/PageCollector.php @@ -8,14 +8,17 @@ class PageCollector extends AbstractCollector /** @inheritdoc */ protected function collect(): array { - global $ID; + $exportID = $this->getConfig()->getExportId(); + if ($exportID === '') { + return []; + } // no export for non existing page - if (!page_exists($ID, $this->rev)) { + if (!page_exists($exportID, $this->rev)) { return []; } - return [$ID]; + return [$exportID]; } } From 92200750cd9cfc4a9489fe7c04c6e41b5a3de6ee Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 19:29:31 +0100 Subject: [PATCH 25/37] Added tests These tests have been initially LLM written, I went through each of them to make sure they make sense and fixed what I thought needed fixing. However coverage is still not great and should be improved in the future. --- .../BookCreatorLiveSelectionCollectorTest.php | 54 +++++++++++ _test/CollectorFactoryTest.php | 90 ++++++++++++++++++ _test/ConfigTest.php | 94 +++++++++++++++++++ _test/LocalContentLoaderTest.php | 47 ++++++++++ _test/MediaLinkResolverTest.php | 20 ++-- _test/NamespaceCollectorCollectTest.php | 62 ++++++++++++ _test/NamespaceCollectorSortTest.php | 8 +- _test/PageCollectorTest.php | 49 ++++++++++ _test/TemplateTest.php | 65 +++++++++++++ _test/WriterInternalLinksTest.php | 71 ++++++++++++++ tpl/default/unittest.html | 17 ++++ 11 files changed, 564 insertions(+), 13 deletions(-) create mode 100644 _test/BookCreatorLiveSelectionCollectorTest.php create mode 100644 _test/CollectorFactoryTest.php create mode 100644 _test/ConfigTest.php create mode 100644 _test/LocalContentLoaderTest.php create mode 100644 _test/NamespaceCollectorCollectTest.php create mode 100644 _test/PageCollectorTest.php create mode 100644 _test/TemplateTest.php create mode 100644 _test/WriterInternalLinksTest.php create mode 100644 tpl/default/unittest.html 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..4283113c --- /dev/null +++ b/_test/ConfigTest.php @@ -0,0 +1,94 @@ + 'Letter', + 'orientation' => 'landscape', + 'font-size' => 14, + 'doublesided' => 0, + 'toc' => 1, + 'toclevels' => '2-4', + 'maxbookmarks' => 3, + 'headernumber' => 1, + 'template' => 'default', + 'usestyles' => 'wrap,foo ', + 'watermark' => 'CONFIDENTIAL', + 'qrcodescale' => '2.5', + ]); + + $this->assertSame('Letter-L', $config->getFormat()); + $this->assertTrue($config->hasToc()); + $this->assertSame(3, $config->getMaxBookmarks()); + $this->assertTrue($config->useNumberedHeaders()); + $this->assertSame(['wrap', 'foo'], $config->getStyledExtensions()); + $this->assertSame('CONFIDENTIAL', $config->getWatermarkText()); + $this->assertSame(2.5, $config->getQRScale()); + + $mpdfConfig = $config->getMPdfConfig(); + $this->assertSame('Letter-L', $mpdfConfig['format']); + $this->assertSame(14, $mpdfConfig['default_font_size']); + $this->assertTrue($mpdfConfig['showWatermarkText']); + $this->assertNotEmpty($config->getCacheKey()); + } + + /** + * Ensure request parameters take precedence over plugin defaults for book export context. + */ + public function testInputOverridesBookParameters(): void + { + global $INPUT; + $INPUT->set('pagesize', 'Legal'); + $INPUT->set('orientation', 'landscape'); + $INPUT->set('font-size', '9'); + $INPUT->set('doublesided', '0'); + $INPUT->set('toclevels', '3-3'); + $INPUT->set('watermark', 'TOPSECRET'); + $INPUT->set('debug', '1'); + $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'); + $INPUT->set('id', 'Playground:Start '); + + $config = new Config(); + + $this->assertSame('Legal-L', $config->getFormat()); + $this->assertTrue($config->isDebugEnabled()); + $this->assertSame('My Book', $config->getBookTitle()); + $this->assertSame('playground:sub', $config->getBookNamespace()); + $this->assertSame('date', $config->getBookSortOrder()); + $this->assertSame(2, $config->getBookNamespaceDepth()); + $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages()); + $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces()); + $this->assertTrue($config->hasLiveSelection()); + $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection()); + $this->assertTrue($config->hasSavedSelection()); + $this->assertSame('fav:123', $config->getSavedSelection()); + $this->assertSame('playground:start', $config->getExportId()); + } +} 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 index 4c0d4652..7f8ce123 100644 --- a/_test/MediaLinkResolverTest.php +++ b/_test/MediaLinkResolverTest.php @@ -15,12 +15,6 @@ class MediaLinkResolverTest extends DokuWikiTest { private $resolver; - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - require_once __DIR__ . '/../vendor/autoload.php'; - } - public function setUp(): void { parent::setUp(); @@ -61,6 +55,9 @@ public function testResolveReturnsLocalPathAndMime(string $input, string $expect $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'); @@ -94,12 +91,19 @@ public function testResolveFetchesExternalMedia(): void $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'); @@ -107,6 +111,9 @@ public function testResolveRejectsNonImages(): void $this->assertNull($resolved); } + /** + * Resizing parameters from fetch.php should trigger scaled copies in cache. + */ public function testResolveAppliesResizeParameter(): void { global $conf; @@ -121,4 +128,3 @@ public function testResolveAppliesResizeParameter(): void $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 index 569571cd..b9b207ac 100644 --- a/_test/NamespaceCollectorSortTest.php +++ b/_test/NamespaceCollectorSortTest.php @@ -12,9 +12,6 @@ */ class NamespaceCollectorSortTest extends DokuWikiTest { - /** @var string[] */ - protected $pluginsEnabled = ['dw2pdf']; - /** * Provide a list of page orderings that should remain stable after sorting. * @@ -67,10 +64,10 @@ public function providerPageNameSort(): array } /** - * @dataProvider providerPageNameSort + * Ensure natural name sorting remains stable for multiple namespace scenarios. * + * @dataProvider providerPageNameSort * @param array $expected - * @return void */ public function testPagenameSort(array $expected): void { @@ -92,4 +89,3 @@ public function testPagenameSort(array $expected): void $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/tpl/default/unittest.html b/tpl/default/unittest.html new file mode 100644 index 00000000..fea862ae --- /dev/null +++ b/tpl/default/unittest.html @@ -0,0 +1,17 @@ + + +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@ From 5340eaffbcc1177667ac835e4e719d9eb8959315 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 19:34:59 +0100 Subject: [PATCH 26/37] rector and codesnifffer fixes --- action.php | 7 ++++--- renderer.php | 1 - src/BookCreatorLiveSelectionCollector.php | 1 - src/BookCreatorSavedSelectionCollector.php | 5 ++--- src/Cache.php | 5 ++--- src/CollectorFactory.php | 8 +++----- src/Config.php | 3 +-- src/HttpClient.php | 6 ++---- src/LocalContentLoader.php | 3 ++- src/MediaLinkResolver.php | 14 ++++++-------- src/NamespaceCollector.php | 8 +++----- src/PageCollector.php | 2 -- src/Styles.php | 3 --- src/Template.php | 2 +- src/Writer.php | 11 +++++------ 15 files changed, 31 insertions(+), 48 deletions(-) diff --git a/action.php b/action.php index 8ba2d8cc..88a19a2f 100644 --- a/action.php +++ b/action.php @@ -26,7 +26,10 @@ */ class action_plugin_dw2pdf extends ActionPlugin { - + /** + * @var int + */ + public $currentBookChapter; /** * Register the events * @@ -175,6 +178,4 @@ public function addsvgbutton(Event $event) array_splice($event->data['items'], -1, 0, [new MenuItem()]); } - - } diff --git a/renderer.php b/renderer.php index c5352f1d..f2c187f6 100644 --- a/renderer.php +++ b/renderer.php @@ -276,7 +276,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/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php index 2b67015f..057c2dba 100644 --- a/src/BookCreatorLiveSelectionCollector.php +++ b/src/BookCreatorLiveSelectionCollector.php @@ -4,7 +4,6 @@ class BookCreatorLiveSelectionCollector extends AbstractCollector { - /** * @inheritdoc * @throws \JsonException diff --git a/src/BookCreatorSavedSelectionCollector.php b/src/BookCreatorSavedSelectionCollector.php index ec716709..00720ddd 100644 --- a/src/BookCreatorSavedSelectionCollector.php +++ b/src/BookCreatorSavedSelectionCollector.php @@ -4,7 +4,6 @@ class BookCreatorSavedSelectionCollector extends AbstractCollector { - /** * @inheritdoc * @throws \JsonException @@ -13,13 +12,13 @@ protected function collect(): array { /** @var action_plugin_bookcreator_handleselection $bcPlugin */ $bcPlugin = plugin_load('action', 'bookcreator_handleselection'); - if( !$bcPlugin ) return []; + if (!$bcPlugin) return []; $savedSelectionId = $this->getConfig()->getSavedSelection(); if ($savedSelectionId === null) return []; $savedselection = $bcPlugin->loadSavedSelection($savedSelectionId); - if(!$this->title && !empty($savedselection['title'])) { + if (!$this->title && !empty($savedselection['title'])) { $this->title = $savedselection['title']; } diff --git a/src/Cache.php b/src/Cache.php index 341d8cc6..004e16e2 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -4,7 +4,6 @@ class Cache extends \dokuwiki\Cache\Cache { - protected AbstractCollector $collector; /** @inheritdoc */ @@ -14,8 +13,8 @@ public function __construct(Config $config, AbstractCollector $collector) $pages = $collector->getPages(); sort($pages); - $key = join(':', [ - join(',', $pages), + $key = implode(':', [ + implode(',', $pages), $config->getCacheKey(), $collector->getTitle(), ]); diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php index af7358ab..650ccfba 100644 --- a/src/CollectorFactory.php +++ b/src/CollectorFactory.php @@ -4,7 +4,6 @@ class CollectorFactory { - /** * Returns the appropriate collector for the given export event * @@ -15,7 +14,7 @@ class CollectorFactory * @throws \InvalidArgumentException If the event is not recognized * @return AbstractCollector */ - static public function create(string $event, Config $config, ?int $rev, ?int $at) + public static function create(string $event, Config $config, ?int $rev, ?int $at) { switch ($event) { case 'export_pdf': @@ -23,9 +22,9 @@ static public function create(string $event, Config $config, ?int $rev, ?int $at case 'export_pdfns': return new NamespaceCollector($config, $rev, $at); // $at would make sense but is not supported yet case 'export_pdfbook': - if( $config->hasLiveSelection() ) { + if ($config->hasLiveSelection()) { return new BookCreatorLiveSelectionCollector($config, $rev, $at); - } elseif($config->hasSavedSelection()) { + } elseif ($config->hasSavedSelection()) { return new BookCreatorSavedSelectionCollector($config, $rev, $at); } // fallthrough @@ -33,5 +32,4 @@ static public function create(string $event, Config $config, ?int $rev, ?int $at throw new \InvalidArgumentException('Invalid export configuration'); } } - } diff --git a/src/Config.php b/src/Config.php index b4235749..c606a8a1 100644 --- a/src/Config.php +++ b/src/Config.php @@ -4,7 +4,6 @@ class Config { - protected string $tempDir = ''; protected string $pagesize = 'A4'; protected bool $isLandscape = false; @@ -188,7 +187,7 @@ public function getQRScale(): float */ public function getCacheKey(): string { - return join(',', [ + return implode(',', [ $this->template, $this->pagesize, $this->isLandscape ? 'L' : 'P', diff --git a/src/HttpClient.php b/src/HttpClient.php index efdd83c8..94d34131 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -2,6 +2,7 @@ namespace dokuwiki\plugin\dw2pdf\src; +use Psr\Http\Message\UriInterface; use dokuwiki\HTTP\DokuHTTPClient; use Mpdf\Http\ClientInterface; use Mpdf\PsrHttpMessageShim\Response; @@ -27,9 +28,6 @@ class HttpClient implements ClientInterface, LoggerAwareInterface public function sendRequest(RequestInterface $request) { $uri = $request->getUri(); - if ($uri === null) { - return new Response(); - } $url = (string)$uri; @@ -56,7 +54,7 @@ public function sendRequest(RequestInterface $request) } $method = strtoupper($request->getMethod()); - $success = $client->sendRequest($url, $body, $method); + $client->sendRequest($url, $body, $method); $response = (new Response())->withStatus($client->status ?: 500); diff --git a/src/LocalContentLoader.php b/src/LocalContentLoader.php index 78d165bc..86f4e439 100644 --- a/src/LocalContentLoader.php +++ b/src/LocalContentLoader.php @@ -18,7 +18,8 @@ class LocalContentLoader implements LocalContentLoaderInterface public function load($path) { // try to translate URLs and fetch.php calls into local cache files - $resolved = $resolved = (new MediaLinkResolver())->resolve($path);; + $resolved = (new MediaLinkResolver())->resolve($path); + ; if ($resolved) { $path = $resolved['path']; } diff --git a/src/MediaLinkResolver.php b/src/MediaLinkResolver.php index ca7e1dd1..a51ee085 100644 --- a/src/MediaLinkResolver.php +++ b/src/MediaLinkResolver.php @@ -9,7 +9,6 @@ */ class MediaLinkResolver { - /** * Resolve a Dokuwiki media URL or local path to a cached file path. * @@ -28,21 +27,20 @@ public function resolve(string $file): ?array if ($mediaID !== null) { [$w, $h, $rev] = $this->extractMediaParams($file); [$ext, $mime] = mimetype($mediaID); - if(!$ext) return null; + if (!$ext) return null; $localFile = $this->localMediaFile($mediaID, $ext, $rev); if (!$localFile) return null; - if(strpos($mime, 'image/') === 0) { + if (str_starts_with($mime, 'image/')) { $localFile = $this->resizedMedia($localFile, $ext, $w, $h); } } else { [, $mime] = mimetype($file); - if (strpos($mime, 'image/') !== 0) return null; + if (!str_starts_with($mime, 'image/')) return null; $localFile = $this->extractLocalImage($file); } if (!$localFile) return null; return ['path' => $localFile, 'mime' => $mime]; - } /** @@ -171,7 +169,7 @@ protected function extractInt(string $subject, string $param): int protected function extractLocalImage($file) { $local = null; - if (substr($file, 0, 9) === 'dw2pdf://') { + if (str_starts_with($file, 'dw2pdf://')) { // support local files passed from plugins $local = substr($file, 9); } elseif (!preg_match('/(\.php|\?)/', $file)) { @@ -180,8 +178,8 @@ protected function extractLocalImage($file) $local = preg_replace("/^$base/i", DOKU_INC, $file, 1); } - if(!file_exists($local)) return null; - if(!is_readable($local)) return null; + if (!file_exists($local)) return null; + if (!is_readable($local)) return null; return $local; } diff --git a/src/NamespaceCollector.php b/src/NamespaceCollector.php index 6ef1121e..026e5d62 100644 --- a/src/NamespaceCollector.php +++ b/src/NamespaceCollector.php @@ -64,7 +64,7 @@ protected function collect(): array // Sort pages, let plugins modify sorting $eventData = ['pages' => &$result, 'sort' => $this->sortorder]; $event = new Event('DW2PDF_NAMESPACEEXPORT_SORT', $eventData); - if($event->advise_before()) { + if ($event->advise_before()) { $result = $this->sortPages($result); } $event->advise_after(); @@ -92,12 +92,10 @@ protected function collect(): array */ protected function excludePages(array $pages) { - $pages = array_filter($pages, function ($page) { - return !in_array($page['id'], $this->excludePages); - }); + $pages = array_filter($pages, fn($page) => !in_array($page['id'], $this->excludePages)); $pages = array_filter($pages, function ($page) { foreach ($this->excludeNamespaces as $ns) { - if (strpos($page['id'], $ns . ':') === 0) { + if (str_starts_with($page['id'], $ns . ':')) { return false; } } diff --git a/src/PageCollector.php b/src/PageCollector.php index 7fa9ab5e..9e02858a 100644 --- a/src/PageCollector.php +++ b/src/PageCollector.php @@ -4,7 +4,6 @@ class PageCollector extends AbstractCollector { - /** @inheritdoc */ protected function collect(): array { @@ -20,5 +19,4 @@ protected function collect(): array return [$exportID]; } - } diff --git a/src/Styles.php b/src/Styles.php index beb523b4..11bc00cb 100644 --- a/src/Styles.php +++ b/src/Styles.php @@ -6,8 +6,6 @@ class Styles { - - protected Config $config; /** @@ -127,5 +125,4 @@ protected function getExtensionStyles() return $list; } - } diff --git a/src/Template.php b/src/Template.php index 831dcb3c..510a0801 100644 --- a/src/Template.php +++ b/src/Template.php @@ -108,7 +108,7 @@ protected function replacePlaceholders(string $html): string global $conf; $params = []; - if(!empty($this->context['at'])) { + if (!empty($this->context['at'])) { $params['at'] = $this->context['at']; } elseif (!empty($this->context['rev'])) { $params['rev'] = $this->context['rev']; diff --git a/src/Writer.php b/src/Writer.php index 37b8f84e..de4595f7 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -120,7 +120,7 @@ public function renderWikiPage(AbstractCollector $collector, string $pageId): vo { $rev = $collector->getRev(); $at = $collector->getAt(); - $file = wikiFN($pageId, ); + $file = wikiFN($pageId,); //ensure $id is in global $ID (needed for parsing) global $ID; @@ -399,11 +399,10 @@ public function getDebugHTML(): string */ protected function write( string $html, - int $mode = HTMLParserMode::DEFAULT_MODE, - bool $init = true, - bool $close = true - ) - { + int $mode = HTMLParserMode::DEFAULT_MODE, + bool $init = true, + bool $close = true + ) { if (!$this->debug) { try { $this->mpdf->WriteHTML($html, $mode, $init, $close); From fbe4e9d9effeaa46d7ace12dca7ec4ca44d83002 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 19:47:37 +0100 Subject: [PATCH 27/37] remove obsolete book chapter handling in action This is now handled within the renderer itself --- action.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/action.php b/action.php index 88a19a2f..735265f5 100644 --- a/action.php +++ b/action.php @@ -26,10 +26,7 @@ */ class action_plugin_dw2pdf extends ActionPlugin { - /** - * @var int - */ - public $currentBookChapter; + /** * Register the events * @@ -45,10 +42,11 @@ 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, $INPUT; + global $REV, $DATE_AT; // our event? $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; @@ -80,11 +78,12 @@ public function convert(Event $event) /** * Build a pdf from the html * + * @param Config $config + * @param AbstractCollector $collector * @param string $cachefile - * @param Event $event * @throws MpdfException */ - protected function generatePDF(Config $config, AbstractCollector $collector, $cachefile, $event) + protected function generatePDF(Config $config, AbstractCollector $collector, string $cachefile) { global $INPUT; @@ -101,10 +100,8 @@ protected function generatePDF(Config $config, AbstractCollector $collector, $ca } // loop over all pages - $counter = 0; foreach ($collector->getPages() as $page) { $template->setContext($collector, $page, $INPUT->server->str('REMOTE_USER', '', true)); - $this->currentBookChapter = $counter++; //FIXME I don't like this $writer->renderWikiPage($collector, $page); } From 7002812751215a8ba31188bf67ee28b885d6aa2c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 26 Nov 2025 20:12:34 +0100 Subject: [PATCH 28/37] extracted PDF generation and sending out of action Now only event handling happens in the action handler --- action.php | 115 ++++---------------------------- src/Config.php | 13 ++++ src/PdfExportService.php | 140 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 101 deletions(-) create mode 100644 src/PdfExportService.php diff --git a/action.php b/action.php index 735265f5..5ad6bf0e 100644 --- a/action.php +++ b/action.php @@ -4,14 +4,10 @@ use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; use dokuwiki\plugin\dw2pdf\MenuItem; -use dokuwiki\plugin\dw2pdf\src\AbstractCollector; use dokuwiki\plugin\dw2pdf\src\Cache; use dokuwiki\plugin\dw2pdf\src\CollectorFactory; use dokuwiki\plugin\dw2pdf\src\Config; -use dokuwiki\plugin\dw2pdf\src\DokuPdf; -use dokuwiki\plugin\dw2pdf\src\Styles; -use dokuwiki\plugin\dw2pdf\src\Template; -use dokuwiki\plugin\dw2pdf\src\Writer; +use dokuwiki\plugin\dw2pdf\src\PdfExportService; use Mpdf\MpdfException; /** @@ -26,7 +22,6 @@ */ class action_plugin_dw2pdf extends ActionPlugin { - /** * Register the events * @@ -46,114 +41,32 @@ public function register(EventHandler $controller) */ public function convert(Event $event) { - global $REV, $DATE_AT; + global $REV, $DATE_AT, $INPUT; // our event? $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; if (!in_array($event->data, $allowedEvents)) { return; } + $event->preventDefault(); + $event->stopPropagation(); + $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); + $pdfService = new PdfExportService( + $config, + $collector, + $cache, + $this->getLang('tocheader'), + $INPUT->server->str('REMOTE_USER', '', true) + ); - if (!$cache->useCache() || $config->isDebugEnabled()) { - // generating the pdf may take a long time for larger wikis / namespaces with many pages - set_time_limit(0); - - // Exceptions bubble up and should be handled by DokuWiki - $this->generatePDF($config, $collector, $cache->cache, $event); - // FIXME there was special handling for BookCreator with $INPUT->has('selection') before - } - - $event->preventDefault(); // after prevent, $event->data cannot be changed - - // deliver the file - $this->sendPDFFile($cache->cache, $collector->getTitle()); //exits - } - - /** - * Build a pdf from the html - * - * @param Config $config - * @param AbstractCollector $collector - * @param string $cachefile - * @throws MpdfException - */ - protected function generatePDF(Config $config, AbstractCollector $collector, string $cachefile) - { - global $INPUT; - - $mpdf = new DokuPDF($config, $collector->getLanguage()); - $styles = new Styles($config); - $template = new Template($config); - $writer = new Writer($mpdf, $config, $template, $styles, $config->isDebugEnabled()); - - $writer->startDocument($collector->getTitle()); - $writer->cover(); - - if ($config->hasToC()) { - $writer->toc($this->getLang('tocheader')); - } - - // loop over all pages - foreach ($collector->getPages() as $page) { - $template->setContext($collector, $page, $INPUT->server->str('REMOTE_USER', '', true)); - $writer->renderWikiPage($collector, $page); - } - - // insert the back page - $writer->back(); - $writer->endDocument(); - - //Return html for debugging - if ($config->isDebugEnabled()) { - header('Content-Type: text/html; charset=utf-8'); - echo $writer->getDebugHTML(); - exit(); - } - - // write to cache file - $mpdf->Output($cachefile, 'F'); - } - - /** - * @param string $cachefile - * @param string $title - */ - protected function sendPDFFile(string $cachefile, string $title) - { - 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($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(); + $cacheFile = $pdfService->getPdf(); // dumps HTML when in debug mode and exits + $pdfService->sendPdf($cacheFile); //exits after sending } diff --git a/src/Config.php b/src/Config.php index c606a8a1..bfd17ce3 100644 --- a/src/Config.php +++ b/src/Config.php @@ -18,6 +18,7 @@ class Config protected bool $isDebug = false; protected array $useStyles = []; protected float $qrCodeScale = 0.0; + protected string $outputTarget = 'file'; // Collector-specific request data protected ?string $bookTitle = null; @@ -68,6 +69,7 @@ public function loadPluginConfig(array $conf) $this->useStyles = array_filter($this->useStyles); } if (isset($conf['watermark'])) $this->watermark = $conf['watermark']; + if (isset($conf['output'])) $this->outputTarget = $conf['output']; if (isset($conf['qrcodescale'])) $this->qrCodeScale = (float)$conf['qrcodescale']; } @@ -92,6 +94,7 @@ public function loadInputConfig() } $this->watermark = $INPUT->str('watermark', $this->watermark); $this->isDebug = $INPUT->bool('debug', $this->isDebug); + $this->outputTarget = $INPUT->str('outputTarget', $this->outputTarget); $this->bookTitle = $INPUT->str('book_title') ?: null; $this->bookNamespace = cleanID($INPUT->str('book_ns')); @@ -180,6 +183,16 @@ 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 * diff --git a/src/PdfExportService.php b/src/PdfExportService.php new file mode 100644 index 00000000..e45f33e5 --- /dev/null +++ b/src/PdfExportService.php @@ -0,0 +1,140 @@ +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 + { + $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, $this->config->isDebugEnabled()); + + $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(); + + if ($this->config->isDebugEnabled()) { + header('Content-Type: text/html; charset=utf-8'); + echo $writer->getDebugHTML(); + exit(); + } + + $mpdf->Output($cacheFile, 'F'); + } +} From 7542e5b4de9152fdc344a41018b4bf9339c6ad4c Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 27 Nov 2025 09:13:05 +0100 Subject: [PATCH 29/37] some minor fixes based on PR feedback --- src/Config.php | 3 ++- src/Writer.php | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Config.php b/src/Config.php index bfd17ce3..6bcd4e7d 100644 --- a/src/Config.php +++ b/src/Config.php @@ -93,6 +93,7 @@ public function loadInputConfig() $this->tocLevels = $this->parseTocLevels($INPUT->str('toclevels')); } $this->watermark = $INPUT->str('watermark', $this->watermark); + $this->template = $INPUT->str('tpl', $this->template, true); $this->isDebug = $INPUT->bool('debug', $this->isDebug); $this->outputTarget = $INPUT->str('outputTarget', $this->outputTarget); @@ -265,7 +266,7 @@ public function getWatermarkText(): string /** * Get all configuration for mpdf as array * - * Note: mode and wrtiting direction are set in DokuPDF based on the language + * 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 diff --git a/src/Writer.php b/src/Writer.php index de4595f7..ec9cadc0 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -91,9 +91,7 @@ public function pageBreak(): void public function wikiPage(string $html): void { $this->conditionalPageBreak(); - $this->applyHeaderFooters(); - $this->write($html, HTMLParserMode::HTML_BODY, false, false); // add citation box if any @@ -262,11 +260,11 @@ public function toc(string $header): void */ public function cover(): void { - $this->conditionalPageBreak(); - $html = $this->template->getHTML('cover'); if (!$html) return; + $this->conditionalPageBreak(); + $this->applyHeaderFooters(); $this->write($html, HTMLParserMode::HTML_BODY, false, false); $this->breakAfterMe(); From 01a7083bc2865eb6d658682a4492ebcc006777a6 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 27 Nov 2025 12:16:01 +0100 Subject: [PATCH 30/37] Use attributes in config class to denote how to initialize them This should make it easier to add new configuration and see with a glance how it is initialized. --- src/Config.php | 137 ++++++++++++++++++++++------------ src/attributes/FromConfig.php | 19 +++++ src/attributes/FromInput.php | 17 +++++ 3 files changed, 127 insertions(+), 46 deletions(-) create mode 100644 src/attributes/FromConfig.php create mode 100644 src/attributes/FromInput.php diff --git a/src/Config.php b/src/Config.php index 6bcd4e7d..697d36f5 100644 --- a/src/Config.php +++ b/src/Config.php @@ -2,33 +2,61 @@ namespace dokuwiki\plugin\dw2pdf\src; +use dokuwiki\plugin\dw2pdf\src\attributes\FromConfig; +use dokuwiki\plugin\dw2pdf\src\attributes\FromInput; + class Config { protected string $tempDir = ''; + + // General PDF configuration + #[FromConfig, FromInput] protected string $pagesize = 'A4'; + #[FromConfig('orientation'), FromInput('orientation')] protected bool $isLandscape = false; + #[FromConfig('font-size'), FromInput('font-size')] protected int $fontSize = 11; + #[FromConfig('doublesided'), FromInput('doublesided')] protected bool $isDoublesided = false; + #[FromConfig('toc')] protected bool $hasToC = false; + #[FromConfig, FromInput('toclevels')] protected array $tocLevels = []; + #[FromConfig] protected int $maxBookmarks = 5; + #[FromConfig('headernumber')] protected bool $numberedHeaders = false; + #[FromConfig, FromInput] protected string $watermark = ''; + #[FromConfig('tpl'), FromInput] protected string $template = 'default'; + #[FromConfig('debug'), FromInput('debug')] protected bool $isDebug = false; + #[FromConfig] protected array $useStyles = []; + #[FromConfig] protected float $qrCodeScale = 0.0; + #[FromConfig, FromInput('outputTarget')] protected string $outputTarget = 'file'; // Collector-specific request data + #[FromConfig, FromInput('book_title')] protected ?string $bookTitle = null; + #[FromConfig, FromInput('book_ns')] protected string $bookNamespace = ''; + #[FromConfig, FromInput('book_order')] protected string $bookSortOrder = 'natural'; + #[FromConfig, FromInput('book_nsdepth')] protected int $bookNamespaceDepth = 0; + #[FromConfig, FromInput('excludes')] protected array $bookExcludePages = []; + #[FromConfig, FromInput('excludesns')] protected array $bookExcludeNamespaces = []; + #[FromConfig, FromInput('selection')] protected ?string $liveSelection = null; + #[FromConfig, FromInput('savedselection')] protected ?string $savedSelection = null; + #[FromConfig, FromInput('id')] protected string $exportId = ''; /** @@ -47,30 +75,60 @@ public function __construct(array $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 parsing + $value = match ($prop) { + 'isLandscape' => ($value === 'landscape'), + 'toclevels' => $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 * - * @param array $conf Plugin configuration + * This will set all properties annotated with FromConfig + * + * @param array $conf (Plugin) configuration */ - public function loadPluginConfig(array $conf) + public function loadPluginConfig(array $conf = []) { - if (isset($conf['pagesize'])) $this->pagesize = $conf['pagesize']; - if (isset($conf['orientation'])) $this->isLandscape = ($conf['orientation'] === 'landscape'); - if (isset($conf['font-size'])) $this->fontSize = (int)$conf['font-size']; - if (isset($conf['doublesided'])) $this->isDoublesided = (bool)$conf['doublesided']; - if (isset($conf['toc'])) $this->hasToC = (bool)$conf['toc']; - if (isset($conf['toclevels'])) $this->tocLevels = $this->parseTocLevels($conf['toclevels']); - if (isset($conf['maxbookmarks'])) $this->maxBookmarks = (int)$conf['maxbookmarks']; - if (isset($conf['headernumber'])) $this->numberedHeaders = (bool)$conf['headernumber']; - if (isset($conf['template'])) $this->template = $conf['template']; - if (isset($conf['usestyles'])) { - $this->useStyles = explode(',', $conf['usestyles']); - $this->useStyles = array_map('trim', $this->useStyles); - $this->useStyles = array_filter($this->useStyles); + $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]); } - if (isset($conf['watermark'])) $this->watermark = $conf['watermark']; - if (isset($conf['output'])) $this->outputTarget = $conf['output']; - if (isset($conf['qrcodescale'])) $this->qrCodeScale = (float)$conf['qrcodescale']; } /** @@ -83,35 +141,22 @@ public function loadPluginConfig(array $conf) public function loadInputConfig() { global $INPUT, $ID; - $this->pagesize = $INPUT->str('pagesize', $this->pagesize); - if ($INPUT->has('orientation')) { - $this->isLandscape = $INPUT->str('orientation') === 'landscape'; - } - $this->fontSize = $INPUT->int('font-size', $this->fontSize); - $this->isDoublesided = $INPUT->bool('doublesided', $this->isDoublesided); - if ($INPUT->has('toclevels')) { - $this->tocLevels = $this->parseTocLevels($INPUT->str('toclevels')); + + 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)); } - $this->watermark = $INPUT->str('watermark', $this->watermark); - $this->template = $INPUT->str('tpl', $this->template, true); - $this->isDebug = $INPUT->bool('debug', $this->isDebug); - $this->outputTarget = $INPUT->str('outputTarget', $this->outputTarget); - - $this->bookTitle = $INPUT->str('book_title') ?: null; - $this->bookNamespace = cleanID($INPUT->str('book_ns')); - $this->bookSortOrder = $INPUT->str('book_order', $this->bookSortOrder, true); - $this->bookNamespaceDepth = max(0, $INPUT->int('book_nsdepth', $this->bookNamespaceDepth)); - $this->bookExcludePages = array_map('cleanID', $INPUT->arr('excludes')); - $this->bookExcludeNamespaces = array_map('cleanID', $INPUT->arr('excludesns')); - - $selection = $INPUT->has('selection') ? $INPUT->str('selection', '', true) : null; - $this->liveSelection = ($selection !== null && $selection !== '') ? $selection : null; - - $saved = $INPUT->has('savedselection') ? $INPUT->str('savedselection') : null; - $this->savedSelection = ($saved !== null && $saved !== '') ? $saved : null; - - $requestID = $INPUT->str('id', $ID ?? '', true); - $this->exportId = cleanID($requestID); } /** 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 @@ + Date: Thu, 27 Nov 2025 13:48:46 +0100 Subject: [PATCH 31/37] make PdfExportService reusable in debug mode This refactors the service so that tests can easily access the produced debug HTML --- src/PdfExportService.php | 47 +++++++++++++++++++++++++++++++--------- src/Styles.php | 4 +++- src/Writer.php | 18 ++++++++++++--- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/PdfExportService.php b/src/PdfExportService.php index e45f33e5..ac71daca 100644 --- a/src/PdfExportService.php +++ b/src/PdfExportService.php @@ -68,7 +68,7 @@ public function getPdf(): string * @return void * @throws MpdfException */ - public function sendPdf(string $cacheFile = null): void + public function sendPdf(?string $cacheFile = null): void { $cacheFile ??= $this->getPdf(); $title = $this->collector->getTitle(); @@ -108,11 +108,45 @@ public function sendPdf(string $cacheFile = null): 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, $this->config->isDebugEnabled()); + $writer = new Writer($mpdf, $this->config, $template, $styles); $writer->startDocument($this->collector->getTitle()); $writer->cover(); @@ -128,13 +162,6 @@ protected function buildDocument(string $cacheFile): void $writer->back(); $writer->endDocument(); - - if ($this->config->isDebugEnabled()) { - header('Content-Type: text/html; charset=utf-8'); - echo $writer->getDebugHTML(); - exit(); - } - - $mpdf->Output($cacheFile, 'F'); + return $writer; } } diff --git a/src/Styles.php b/src/Styles.php index 11bc00cb..d8d40c00 100644 --- a/src/Styles.php +++ b/src/Styles.php @@ -26,7 +26,9 @@ public function __construct(Config $config) public function getCSS(): string { //reuse the CSS dispatcher functions without triggering the main function - define('SIMPLE_TEST', 1); + if (!defined('SIMPLE_TEST')) { + define('SIMPLE_TEST', 1); + } require_once(DOKU_INC . 'lib/exe/css.php'); // prepare CSS files diff --git a/src/Writer.php b/src/Writer.php index ec9cadc0..e9c6d148 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -36,15 +36,15 @@ class Writer /** * @param DokuPdf $mpdf * @param Template $template - * @param bool $debug + * @param Styles $styles */ - public function __construct(DokuPdf $mpdf, Config $config, Template $template, Styles $styles, bool $debug = false) + public function __construct(DokuPdf $mpdf, Config $config, Template $template, Styles $styles) { $this->mpdf = $mpdf; $this->config = $config; $this->template = $template; $this->styles = $styles; - $this->debug = $debug; + $this->debug = $config->isDebugEnabled(); } /** @@ -384,6 +384,18 @@ 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 * From a9e9f3349cf39531b2424a4af18ce013fd76a875 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 27 Nov 2025 13:50:03 +0100 Subject: [PATCH 32/37] introduce a first test for rendering - testing numbered headlines The test passes, but I am not 100% sure the behaviour is correct yet. Static variables in the renderer might be a problem. Not tested with multiple documents yet. Currently render options need to be set via global conf and are not passed from the Config object. --- _test/EndToEndTest.php | 71 +++++++++++++++++++++++++++++++++++++++++ _test/pages/headers.txt | 17 ++++++++++ renderer.php | 9 ++++-- 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 _test/EndToEndTest.php create mode 100644 _test/pages/headers.txt diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php new file mode 100644 index 00000000..73c73526 --- /dev/null +++ b/_test/EndToEndTest.php @@ -0,0 +1,71 @@ + 1, 'exportid' => $pageId] + )); + $collector = new PageCollector($config); + $cache = new Cache($config, $collector); + $service = new PdfExportService($config, $collector, $cache, 'Contents', 'tester'); + return $service->getDebugHtml(); + } + + + public function testNumberedHeaders(): void + { + global $conf; + $conf['plugin']['dw2pdf']['headernumber'] = 1; // Currently Config values are not passed to the renderer + $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()); + }); + } +} 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/renderer.php b/renderer.php index f2c187f6..d1547880 100644 --- a/renderer.php +++ b/renderer.php @@ -34,8 +34,8 @@ public function document_start() $this->doc .= ""; static $chapter = 0; // FIXME we can probably do without a static here and use a class property - $chapter++; self::$header_count[1] = $chapter; + $chapter++; } /** @@ -80,7 +80,7 @@ public function header($text, $level, $pos, $returnonly = false) // retrieve numbered headings option $isnumberedheadings = $this->getConf('headernumber'); - $header_prefix = ""; + $header_prefix = ''; if ($isnumberedheadings) { if ($level > 0) { if (self::$previous_level > $level) { @@ -89,13 +89,16 @@ public function header($text, $level, $pos, $returnonly = false) } } } - self::$header_count[$level]++; + self::$header_count[$level] = (self::$header_count[$level] ?? 0) + 1; // $header_prefix = ""; for ($i = 1; $i <= $level; $i++) { $header_prefix .= self::$header_count[$i] . "."; } } + if($header_prefix !== '') { + $header_prefix .= ' '; + } // add PDF bookmark $bookmark = ''; From bee95f00a2aae6303d6089bcb1a6d4a70c3c681b Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 27 Nov 2025 14:57:37 +0100 Subject: [PATCH 33/37] Overhaul chapter/header counting in renderer Using static variables makes it impossible to automatically test this correctly. The PDF renderer is now a singleton. The Writer class initializes it anew for each export, but during the export, the same renderer is reused. --- _test/EndToEndTest.php | 48 +++++++++++++++++++++++++++++++++--------- _test/pages/simple.txt | 3 +++ renderer.php | 33 +++++++++++++++++++---------- src/Writer.php | 4 ++++ 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 _test/pages/simple.txt diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php index 73c73526..4a3eac14 100644 --- a/_test/EndToEndTest.php +++ b/_test/EndToEndTest.php @@ -2,6 +2,7 @@ namespace dokuwiki\plugin\dw2pdf\test; +use dokuwiki\plugin\dw2pdf\src\BookCreatorLiveSelectionCollector; use dokuwiki\plugin\dw2pdf\src\Cache; use dokuwiki\plugin\dw2pdf\src\Config; use dokuwiki\plugin\dw2pdf\src\PageCollector; @@ -26,28 +27,36 @@ public function setUp(): void /** - * Create the page, render it through the PdfExportService in debug mode and return the resulting HTML. + * Create the pages, render them through the PdfExportService in debug mode and return the resulting HTML. * - * @param string $pageId - * @return string - * @throws \Mpdf\MpdfException + * @param string|string[] $pages One or more pages to be included in the export + * @return string Rendered HTML output */ - protected function getDebugHTML(string $pageId, $conf = []): string + protected function getDebugHTML($pages, $conf = []): string { - $data = file_get_contents(__DIR__ . '/pages/' . $pageId . '.txt'); - saveWikiText($pageId, $data, 'dw2pdf end-to-end test'); + $pages = (array)$pages; + + foreach ($pages as $page) { + $data = file_get_contents(__DIR__ . '/pages/' . $page . '.txt'); + saveWikiText($page, $data, 'dw2pdf end-to-end test'); + } $config = new Config(array_merge( $conf, - ['debug' => 1, 'exportid' => $pageId] + [ + 'debug' => 1, + 'liveselection' => json_encode($pages) + ] )); - $collector = new PageCollector($config); + $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 { global $conf; @@ -68,4 +77,23 @@ public function testNumberedHeaders(): void $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 + { + global $conf; + $conf['plugin']['dw2pdf']['headernumber'] = 1; // Currently Config values are not passed to the renderer + $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()); + }); + } } 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/renderer.php b/renderer.php index d1547880..6f1beb72 100644 --- a/renderer.php +++ b/renderer.php @@ -17,8 +17,19 @@ 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; + + /** + * The Writer will reinitialize the renderer for each export, but the object will be reused within one export. + * + * @inheritdoc + */ + public function isSingleton() + { + return true; + } public function document_start() { @@ -33,9 +44,9 @@ public function document_start() $this->doc .= ""; $this->doc .= ""; - static $chapter = 0; // FIXME we can probably do without a static here and use a class property - self::$header_count[1] = $chapter; - $chapter++; + + $this->header_count[1] = $this->chapter; + $this->chapter++; } /** @@ -83,17 +94,17 @@ public function header($text, $level, $pos, $returnonly = false) $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] = (self::$header_count[$level] ?? 0) + 1; + $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 !== '') { @@ -120,7 +131,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; } /** diff --git a/src/Writer.php b/src/Writer.php index e9c6d148..f0a578ee 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -45,6 +45,10 @@ public function __construct(DokuPdf $mpdf, Config $config, Template $template, S $this->template = $template; $this->styles = $styles; $this->debug = $config->isDebugEnabled(); + + // initialize a new renderer instance (singleton instance will be reused in later p_* calls) + $renderer = plugin_load('renderer', 'dw2pdf', true); + // FIXME set configuration on the renderer here } /** From b08d547052e5b3f881c522cb47ff9b7a201195e1 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Thu, 27 Nov 2025 15:09:38 +0100 Subject: [PATCH 34/37] pass the Config object into the renderer This streamlines the configuration to a single point of entry. The renderer was the last bit not adhering to that. --- _test/EndToEndTest.php | 4 ---- renderer.php | 20 ++++++++++++++++++-- src/Writer.php | 7 +++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php index 4a3eac14..30e06617 100644 --- a/_test/EndToEndTest.php +++ b/_test/EndToEndTest.php @@ -59,8 +59,6 @@ protected function getDebugHTML($pages, $conf = []): string */ public function testNumberedHeaders(): void { - global $conf; - $conf['plugin']['dw2pdf']['headernumber'] = 1; // Currently Config values are not passed to the renderer $html = $this->getDebugHTML('headers', ['headernumber' => 1]); $dom = (new Document())->html($html); @@ -85,8 +83,6 @@ public function testNumberedHeaders(): void */ public function testNumberedHeadersMultipage(): void { - global $conf; - $conf['plugin']['dw2pdf']['headernumber'] = 1; // Currently Config values are not passed to the renderer $html = $this->getDebugHTML(['headers', 'simple'], ['headernumber' => 1]); $dom = (new Document())->html($html); diff --git a/renderer.php b/renderer.php index 6f1beb72..6e553f52 100644 --- a/renderer.php +++ b/renderer.php @@ -20,6 +20,7 @@ class renderer_plugin_dw2pdf extends Doku_Renderer_xhtml private $header_count = []; private $previous_level = 0; private int $chapter = 0; + private ?Config $config; /** * The Writer will reinitialize the renderer for each export, but the object will be reused within one export. @@ -31,10 +32,25 @@ public function isSingleton() return true; } + /** + * Set the active configuration + * + * @param Config $config + * @return void + */ + public function setConfig(Config $config): void + { + $this->config = $config; + } + public function document_start() { global $ID; + if($this->config === null) { + throw new RuntimeException('DW2PDF Renderer configuration not set'); + } + parent::document_start(); //anchor for rewritten links to included pages @@ -89,7 +105,7 @@ public function header($text, $level, $pos, $returnonly = false) // retrieve numbered headings option - $isnumberedheadings = $this->getConf('headernumber'); + $isnumberedheadings = $this->config->useNumberedHeaders(); $header_prefix = ''; if ($isnumberedheadings) { @@ -113,7 +129,7 @@ public function header($text, $level, $pos, $returnonly = false) // add PDF bookmark $bookmark = ''; - $maxbookmarklevel = $this->getConf('maxbookmarks'); + $maxbookmarklevel = $this->config->getMaxBookmarks(); // 0: off, 1-6: show down to this level if ($maxbookmarklevel && $maxbookmarklevel >= $level) { $bookmarklevel = $this->calculateBookmarklevel($level); diff --git a/src/Writer.php b/src/Writer.php index f0a578ee..645440c7 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -46,9 +46,12 @@ public function __construct(DokuPdf $mpdf, Config $config, Template $template, S $this->styles = $styles; $this->debug = $config->isDebugEnabled(); - // initialize a new renderer instance (singleton instance will be reused in later p_* calls) + /** + * 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); - // FIXME set configuration on the renderer here + $renderer->setConfig($config); } /** From c124fc539f0c5c773f84bc64bb897525f386e8f8 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Fri, 28 Nov 2025 17:13:50 +0100 Subject: [PATCH 35/37] Some more tests testing rendering functionality Again mostly LLM generated but manually checked and corrected. --- _test/EndToEndTest.php | 201 +++++++++++++++++++++++++++++- _test/pages/renderer_features.txt | 29 +++++ _test/pages/target.txt | 5 + 3 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 _test/pages/renderer_features.txt create mode 100644 _test/pages/target.txt diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php index 30e06617..0fbf9317 100644 --- a/_test/EndToEndTest.php +++ b/_test/EndToEndTest.php @@ -8,6 +8,7 @@ use dokuwiki\plugin\dw2pdf\src\PageCollector; use dokuwiki\plugin\dw2pdf\src\PdfExportService; use DOMWrap\Document; +use DOMWrap\Element; /** * End-to-end tests for the dw2pdf plugin @@ -37,8 +38,7 @@ protected function getDebugHTML($pages, $conf = []): string $pages = (array)$pages; foreach ($pages as $page) { - $data = file_get_contents(__DIR__ . '/pages/' . $page . '.txt'); - saveWikiText($page, $data, 'dw2pdf end-to-end test'); + $this->prepareFixturePage($page); } $config = new Config(array_merge( @@ -92,4 +92,201 @@ public function testNumberedHeadersMultipage(): void $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/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/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. From 212a1ea54602e857db1c810a68e294f2d9486e20 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Fri, 28 Nov 2025 18:20:26 +0100 Subject: [PATCH 36/37] fix config attrbutes and extend tests --- _test/ConfigTest.php | 170 ++++++++++++++++++++++++++++++++++--------- conf/default.php | 2 +- src/Config.php | 18 ++--- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/_test/ConfigTest.php b/_test/ConfigTest.php index 4283113c..a0342713 100644 --- a/_test/ConfigTest.php +++ b/_test/ConfigTest.php @@ -18,12 +18,63 @@ public function setUp(): void } /** - * Ensure plugin.conf overrides and transformations are honored before reading user input. + * Check default values as set in the Config class */ - public function testPluginConfigurationOverridesDefaults(): void + public function testDefaults(): void + { + global $conf, $ID; + $ID = ''; + + $config = new Config(); + $mpdfConfig = $config->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([ - 'pagesize' => 'Letter', + 'exportid' => 'playground:start', + 'pagesize' => 'Legal', 'orientation' => 'landscape', 'font-size' => 14, 'doublesided' => 0, @@ -31,40 +82,78 @@ public function testPluginConfigurationOverridesDefaults(): void 'toclevels' => '2-4', 'maxbookmarks' => 3, 'headernumber' => 1, - 'template' => 'default', + '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('Letter-L', $config->getFormat()); - $this->assertTrue($config->hasToc()); - $this->assertSame(3, $config->getMaxBookmarks()); - $this->assertTrue($config->useNumberedHeaders()); - $this->assertSame(['wrap', 'foo'], $config->getStyledExtensions()); - $this->assertSame('CONFIDENTIAL', $config->getWatermarkText()); - $this->assertSame(2.5, $config->getQRScale()); + $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('Letter-L', $mpdfConfig['format']); - $this->assertSame(14, $mpdfConfig['default_font_size']); - $this->assertTrue($mpdfConfig['showWatermarkText']); - $this->assertNotEmpty($config->getCacheKey()); + $this->assertSame(['H1' => 0, 'H2' => 1, 'H3' => 2], $mpdfConfig['h2toc'], 'from toclevels'); } /** - * Ensure request parameters take precedence over plugin defaults for book export context. + * Ensure request parameters take precedence over defaults */ - public function testInputOverridesBookParameters(): void + public function testloadInputConfig(): void { - global $INPUT; + global $INPUT, $ID; + $ID = 'playground:start'; $INPUT->set('pagesize', 'Legal'); $INPUT->set('orientation', 'landscape'); - $INPUT->set('font-size', '9'); + $INPUT->set('font-size', '14'); $INPUT->set('doublesided', '0'); - $INPUT->set('toclevels', '3-3'); - $INPUT->set('watermark', 'TOPSECRET'); + $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'); @@ -73,22 +162,33 @@ public function testInputOverridesBookParameters(): void $INPUT->set('excludesns', ['playground:private']); $INPUT->set('selection', '["playground:start","playground:Sub:Child"]'); $INPUT->set('savedselection', 'fav:123'); - $INPUT->set('id', 'Playground:Start '); + $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'); - $this->assertSame('Legal-L', $config->getFormat()); - $this->assertTrue($config->isDebugEnabled()); - $this->assertSame('My Book', $config->getBookTitle()); - $this->assertSame('playground:sub', $config->getBookNamespace()); - $this->assertSame('date', $config->getBookSortOrder()); - $this->assertSame(2, $config->getBookNamespaceDepth()); - $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages()); - $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces()); - $this->assertTrue($config->hasLiveSelection()); - $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection()); - $this->assertTrue($config->hasSavedSelection()); - $this->assertSame('fav:123', $config->getSavedSelection()); - $this->assertSame('playground:start', $config->getExportId()); } + + } diff --git a/conf/default.php b/conf/default.php index c088360c..df4ff749 100644 --- a/conf/default.php +++ b/conf/default.php @@ -10,7 +10,7 @@ $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/src/Config.php b/src/Config.php index 697d36f5..a6375532 100644 --- a/src/Config.php +++ b/src/Config.php @@ -18,7 +18,7 @@ class Config protected int $fontSize = 11; #[FromConfig('doublesided'), FromInput('doublesided')] protected bool $isDoublesided = false; - #[FromConfig('toc')] + #[FromConfig('toc'), FromInput('toc')] protected bool $hasToC = false; #[FromConfig, FromInput('toclevels')] protected array $tocLevels = []; @@ -28,7 +28,7 @@ class Config protected bool $numberedHeaders = false; #[FromConfig, FromInput] protected string $watermark = ''; - #[FromConfig('tpl'), FromInput] + #[FromConfig, FromInput('tpl')] protected string $template = 'default'; #[FromConfig('debug'), FromInput('debug')] protected bool $isDebug = false; @@ -36,7 +36,7 @@ class Config protected array $useStyles = []; #[FromConfig] protected float $qrCodeScale = 0.0; - #[FromConfig, FromInput('outputTarget')] + #[FromConfig('output'), FromInput('outputTarget')] protected string $outputTarget = 'file'; // Collector-specific request data @@ -56,7 +56,7 @@ class Config protected ?string $liveSelection = null; #[FromConfig, FromInput('savedselection')] protected ?string $savedSelection = null; - #[FromConfig, FromInput('id')] + #[FromConfig] // also read from global $ID protected string $exportId = ''; /** @@ -87,11 +87,11 @@ public function __construct(array $pluginConf = []) */ protected function setProperty(string $prop, ?string $type, $value) { - // custom parsing - $value = match ($prop) { - 'isLandscape' => ($value === 'landscape'), - 'toclevels' => $this->parseTocLevels((string)$value), - 'exportId' => cleanID((string)$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, }; From 837fcadd5edb939d2ba3977f363c3478f965471b Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Tue, 2 Dec 2025 12:57:15 +0100 Subject: [PATCH 37/37] update MPDF downgrade log/psr mpdf v8.2.6 => v8.2.7 log 3.0.2 => 1.1.4 Using the old log version avoids conflicts with the smtp plugin for now. However we need a proper solution to this problem in the future (probably in core). A PR to be able to use any log version with the smtp plugin has been submitted: https://github.com/txthinking/Mailer/pull/36 We probably should introduce a log/psr:3 dependency in core when upgrading to PHP8 (and provide a PSR compatible wrapper around our logger) --- composer.json | 3 +- composer.lock | 49 +++++----- vendor/composer/autoload_psr4.php | 2 +- vendor/composer/autoload_static.php | 2 +- vendor/composer/installed.json | 48 ++++----- vendor/composer/installed.php | 22 ++--- vendor/composer/platform_check.php | 4 +- vendor/mpdf/mpdf/.gitignore | 1 + vendor/mpdf/mpdf/CHANGELOG.md | 1 + vendor/mpdf/mpdf/composer.json | 2 +- vendor/mpdf/mpdf/phpstan-baseline.neon | 86 +--------------- vendor/mpdf/mpdf/src/Cache.php | 30 ++++-- vendor/mpdf/mpdf/src/Http/CurlHttpClient.php | 12 ++- vendor/mpdf/mpdf/src/Mpdf.php | 59 ++++++----- vendor/mpdf/mpdf/src/SizeConverter.php | 1 + vendor/mpdf/mpdf/src/Writer/FontWriter.php | 18 ++-- vendor/mpdf/psr-log-aware-trait/composer.json | 2 +- .../src/MpdfPsrLogAwareTrait.php | 2 +- .../src/PsrLogAwareTrait.php | 2 +- vendor/psr/log/composer.json | 6 +- vendor/psr/log/src/AbstractLogger.php | 15 --- .../psr/log/src/InvalidArgumentException.php | 7 -- vendor/psr/log/src/LogLevel.php | 18 ---- vendor/psr/log/src/LoggerAwareInterface.php | 14 --- vendor/psr/log/src/LoggerAwareTrait.php | 22 ----- vendor/psr/log/src/LoggerInterface.php | 98 ------------------- vendor/psr/log/src/LoggerTrait.php | 98 ------------------- vendor/psr/log/src/NullLogger.php | 26 ----- 28 files changed, 160 insertions(+), 490 deletions(-) delete mode 100644 vendor/psr/log/src/AbstractLogger.php delete mode 100644 vendor/psr/log/src/InvalidArgumentException.php delete mode 100644 vendor/psr/log/src/LogLevel.php delete mode 100644 vendor/psr/log/src/LoggerAwareInterface.php delete mode 100644 vendor/psr/log/src/LoggerAwareTrait.php delete mode 100644 vendor/psr/log/src/LoggerInterface.php delete mode 100644 vendor/psr/log/src/LoggerTrait.php delete mode 100644 vendor/psr/log/src/NullLogger.php diff --git a/composer.json b/composer.json index 3f58eba4..0b526e85 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "ext-dom": "*", "ext-libxml": "*", "mpdf/mpdf": "8.2.*", - "mpdf/qrcode": "^1.2" + "mpdf/qrcode": "^1.2", + "psr/log": "^1.0" }, "replace": { "paragonie/random_compat": "*" diff --git a/composer.lock b/composer.lock index a228a7fa..30af5c10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "967c0fd288a996c842044c1583092191", + "content-hash": "70e3f425d861b01c41a304e53af0cadc", "packages": [ { "name": "mpdf/mpdf", - "version": "v8.2.6", + "version": "v8.2.7", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { @@ -85,7 +85,7 @@ "type": "custom" } ], - "time": "2025-08-18T08:51:51+00:00" + "time": "2025-12-01T10:18:02+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -137,20 +137,20 @@ }, { "name": "mpdf/psr-log-aware-trait", - "version": "v3.0.0", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/mpdf/psr-log-aware-trait.git", - "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", - "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275", "shasum": "" }, "require": { - "psr/log": "^3.0" + "psr/log": "^1.0 || ^2.0" }, "type": "library", "autoload": { @@ -175,9 +175,9 @@ "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/v3.0.0" + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0" }, - "time": "2023-05-03T06:19:36+00:00" + "time": "2023-05-03T06:18:28+00:00" }, { "name": "mpdf/qrcode", @@ -362,30 +362,30 @@ }, { "name": "psr/log", - "version": "3.0.2", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -406,9 +406,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "setasign/fpdi", @@ -489,7 +489,10 @@ "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": {}, + "platform": { + "ext-dom": "*", + "ext-libxml": "*" + }, "platform-dev": {}, "platform-overrides": { "php": "8.0" diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index bc5ce2b5..1baeb5f1 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,7 +7,7 @@ return array( 'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'), - 'Psr\\Log\\' => array($vendorDir . '/psr/log/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'), diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 1aeaad1b..d22f2282 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -41,7 +41,7 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04 ), 'Psr\\Log\\' => array ( - 0 => __DIR__ . '/..' . '/psr/log/src', + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), 'Psr\\Http\\Message\\' => array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 8fcbe7c8..eb5ca37b 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,17 +2,17 @@ "packages": [ { "name": "mpdf/mpdf", - "version": "v8.2.6", - "version_normalized": "8.2.6.0", + "version": "v8.2.7", + "version_normalized": "8.2.7.0", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { @@ -39,7 +39,7 @@ "ext-xml": "Needed mainly for SVG manipulation", "ext-zlib": "Needed for compression of embedded resources, such as fonts" }, - "time": "2025-08-18T08:51:51+00:00", + "time": "2025-12-01T10:18:02+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -137,23 +137,23 @@ }, { "name": "mpdf/psr-log-aware-trait", - "version": "v3.0.0", - "version_normalized": "3.0.0.0", + "version": "v2.0.0", + "version_normalized": "2.0.0.0", "source": { "type": "git", "url": "https://github.com/mpdf/psr-log-aware-trait.git", - "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", - "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275", "shasum": "" }, "require": { - "psr/log": "^3.0" + "psr/log": "^1.0 || ^2.0" }, - "time": "2023-05-03T06:19:36+00:00", + "time": "2023-05-03T06:18:28+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -178,7 +178,7 @@ "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/v3.0.0" + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0" }, "install-path": "../mpdf/psr-log-aware-trait" }, @@ -374,33 +374,33 @@ }, { "name": "psr/log", - "version": "3.0.2", - "version_normalized": "3.0.2.0", + "version": "1.1.4", + "version_normalized": "1.1.4.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, - "time": "2024-09-11T13:17:53+00:00", + "time": "2021-05-03T11:20:27+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -421,7 +421,7 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, "install-path": "../psr/log" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 5d3b489e..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' => '27b69b62ea8ff7c01f156d4b8719d904bbc912cf', + 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,9 +11,9 @@ ), 'versions' => array( 'mpdf/mpdf' => array( - 'pretty_version' => 'v8.2.6', - 'version' => '8.2.6.0', - 'reference' => 'dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44', + 'pretty_version' => 'v8.2.7', + 'version' => '8.2.7.0', + 'reference' => 'b59670a09498689c33ce639bac8f5ba26721dab3', 'type' => 'library', 'install_path' => __DIR__ . '/../mpdf/mpdf', 'aliases' => array(), @@ -29,9 +29,9 @@ 'dev_requirement' => false, ), 'mpdf/psr-log-aware-trait' => array( - 'pretty_version' => 'v3.0.0', - 'version' => '3.0.0.0', - 'reference' => 'a633da6065e946cc491e1c962850344bb0bf3e78', + 'pretty_version' => 'v2.0.0', + 'version' => '2.0.0.0', + 'reference' => '7a077416e8f39eb626dee4246e0af99dd9ace275', 'type' => 'library', 'install_path' => __DIR__ . '/../mpdf/psr-log-aware-trait', 'aliases' => array(), @@ -71,9 +71,9 @@ 'dev_requirement' => false, ), 'psr/log' => array( - 'pretty_version' => '3.0.2', - 'version' => '3.0.2.0', - 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), @@ -91,7 +91,7 @@ 'splitbrain/dw2pdf' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '27b69b62ea8ff7c01f156d4b8719d904bbc912cf', + 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index a70ba47c..6cd6b536 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 80000)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.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) { 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 77734670..4c7b13fd 100644 --- a/vendor/mpdf/mpdf/CHANGELOG.md +++ b/vendor/mpdf/mpdf/CHANGELOG.md @@ -13,6 +13,7 @@ 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 =========================== diff --git a/vendor/mpdf/mpdf/composer.json b/vendor/mpdf/mpdf/composer.json index cb1c5f4e..a8481f86 100644 --- a/vendor/mpdf/mpdf/composer.json +++ b/vendor/mpdf/mpdf/composer.json @@ -64,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 index 7a9bf59c..eed44e96 100644 --- a/vendor/mpdf/mpdf/phpstan-baseline.neon +++ b/vendor/mpdf/mpdf/phpstan-baseline.neon @@ -84,24 +84,12 @@ parameters: count: 10 path: src/Form.php - - - message: '#^Variable \$dif might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/Gif/ColorTable.php - - message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#' identifier: binaryOp.invalid count: 1 path: src/Gradient.php - - - message: '#^Comparison operation "\<" between \(array\|float\|int\) and 1 results in an error\.$#' - identifier: smaller.invalid - count: 1 - path: src/Gradient.php - - message: '#^Variable \$angle in isset\(\) always exists and is not nullable\.$#' identifier: isset.variable @@ -192,12 +180,6 @@ parameters: count: 2 path: src/Image/ImageProcessor.php - - - message: '#^Binary operation "\*" between \(list\\|string\|null\) and \-1 results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Image/Svg.php - - message: '#^Binary operation "\*" between 0\.3333333333333333 and string results in an error\.$#' identifier: binaryOp.invalid @@ -348,30 +330,6 @@ parameters: count: 1 path: src/Mpdf.php - - - message: '#^Comparison operation "\<" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' - identifier: smaller.invalid - count: 2 - path: src/Mpdf.php - - - - message: '#^Comparison operation "\>" between \(array\|float\|int\) and \(float\|int\) results in an error\.$#' - identifier: greater.invalid - count: 1 - path: src/Mpdf.php - - - - message: '#^Comparison operation "\>" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' - identifier: greater.invalid - count: 4 - path: src/Mpdf.php - - - - message: '#^Comparison operation "\>" between array\|float\|int and 0 results in an error\.$#' - identifier: greater.invalid - count: 2 - 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 @@ -741,7 +699,7 @@ parameters: - message: '#^Variable \$p might not be defined\.$#' identifier: variable.undefined - count: 2 + count: 1 path: src/Mpdf.php - @@ -924,24 +882,6 @@ parameters: count: 1 path: src/Otl.php - - - message: '#^Comparison operation "\>" between array\|float\|int\|string\|false\|null and 0 results in an error\.$#' - identifier: greater.invalid - count: 5 - path: src/Otl.php - - - - message: '#^Comparison operation "\>\=" between int and \(array\|float\|int\) results in an error\.$#' - identifier: greaterOrEqual.invalid - count: 1 - path: src/Otl.php - - - - message: '#^Comparison operation "\>\=" between int\<0, max\> and \(array\|float\|int\) results in an error\.$#' - identifier: greaterOrEqual.invalid - count: 1 - path: src/Otl.php - - message: '#^Offset int on array\{\} in isset\(\) does not exist\.$#' identifier: isset.offset @@ -1023,19 +963,19 @@ parameters: - message: '#^Variable \$PosLookupRecord might not be defined\.$#' identifier: variable.undefined - count: 3 + count: 1 path: src/Otl.php - message: '#^Variable \$SequenceIndex might not be defined\.$#' identifier: variable.undefined - count: 8 + count: 4 path: src/Otl.php - message: '#^Variable \$SubstLookupRecord might not be defined\.$#' identifier: variable.undefined - count: 6 + count: 2 path: src/Otl.php - @@ -1092,12 +1032,6 @@ parameters: count: 1 path: src/Otl.php - - - message: '#^Comparison operation "\>\=" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' - identifier: greaterOrEqual.invalid - count: 1 - path: src/OtlDump.php - - message: '#^Undefined variable\: \$rtlPUAarr$#' identifier: variable.undefined @@ -1170,12 +1104,6 @@ parameters: count: 2 path: src/Shaper/Indic.php - - - message: '#^Comparison operation "\>\=" between \(float\|int\) and \(array\|float\|int\) results in an error\.$#' - identifier: greaterOrEqual.invalid - count: 1 - path: src/TTFontFile.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 @@ -1278,12 +1206,6 @@ parameters: count: 1 path: src/TTFontFileAnalysis.php - - - message: '#^Comparison operation "\>\=" between \(float\|int\) and \(non\-empty\-array\|float\|int\\|int\<1, max\>\) results in an error\.$#' - identifier: greaterOrEqual.invalid - count: 1 - path: src/TTFontFileAnalysis.php - - message: '#^Variable \$TOC_end might not be defined\.$#' identifier: variable.undefined 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/Http/CurlHttpClient.php b/vendor/mpdf/mpdf/src/Http/CurlHttpClient.php index 2c5dc454..803a3f38 100644 --- a/vendor/mpdf/mpdf/src/Http/CurlHttpClient.php +++ b/vendor/mpdf/mpdf/src/Http/CurlHttpClient.php @@ -92,7 +92,7 @@ static function ($curl, $header) use (&$response) { throw new \Mpdf\MpdfException($message); } - curl_close($ch); + $this->closeCurl($ch); return $response; } @@ -106,16 +106,22 @@ static function ($curl, $header) use (&$response) { throw new \Mpdf\MpdfException($message); } - curl_close($ch); + $this->closeCurl($ch); return $response->withStatus($info['http_code']); } - curl_close($ch); + $this->closeCurl($ch); return $response ->withStatus($info['http_code']) ->withBody(Stream::create($data)); } + private function closeCurl($ch) + { + if (PHP_VERSION_ID < 80000) { + curl_close($ch); + } + } } diff --git a/vendor/mpdf/mpdf/src/Mpdf.php b/vendor/mpdf/mpdf/src/Mpdf.php index e361b973..b4403035 100644 --- a/vendor/mpdf/mpdf/src/Mpdf.php +++ b/vendor/mpdf/mpdf/src/Mpdf.php @@ -32,7 +32,7 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface use FpdiTrait; use MpdfPsrLogAwareTrait; - const VERSION = '8.2.6'; + const VERSION = '8.2.7'; const SCALE = 72 / 25.4; @@ -1913,6 +1913,11 @@ function SetAlpha($alpha, $bm = 'Normal', $return = false, $mode = 'B') } } + /** + * @param mixed[] $parms + * + * @return int + */ function AddExtGState($parms) { $n = count($this->extgstates); @@ -1933,6 +1938,7 @@ function AddExtGState($parms) } $n++; $this->extgstates[$n]['parms'] = $parms; + return $n; } @@ -10037,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 @@ -13095,7 +13102,7 @@ function SetWatermarkText($txt = '', $alpha = -1) $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_font = $txt->getFont() !== null ? $txt->getFont() : $this->watermark_font; $this->watermark_size = $txt->getSize(); return; @@ -22245,6 +22252,11 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl // 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; @@ -23889,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]); } } @@ -23922,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]; } @@ -23956,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]; } @@ -23969,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]; @@ -23982,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]; } @@ -23994,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]); } @@ -24003,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]); } @@ -24012,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]); } @@ -24025,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]; @@ -24039,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]); } diff --git a/vendor/mpdf/mpdf/src/SizeConverter.php b/vendor/mpdf/mpdf/src/SizeConverter.php index 11175446..15a51873 100644 --- a/vendor/mpdf/mpdf/src/SizeConverter.php +++ b/vendor/mpdf/mpdf/src/SizeConverter.php @@ -70,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/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/psr-log-aware-trait/composer.json b/vendor/mpdf/psr-log-aware-trait/composer.json index 1abd3f6f..daf1c388 100644 --- a/vendor/mpdf/psr-log-aware-trait/composer.json +++ b/vendor/mpdf/psr-log-aware-trait/composer.json @@ -3,7 +3,7 @@ "description": "Trait to allow support of different psr/log versions.", "type": "library", "require": { - "psr/log": "^3.0" + "psr/log": "^1.0 || ^2.0" }, "license": "MIT", "autoload": { diff --git a/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php b/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php index b4e5b7d5..4900229e 100644 --- a/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php +++ b/vendor/mpdf/psr-log-aware-trait/src/MpdfPsrLogAwareTrait.php @@ -12,7 +12,7 @@ trait MpdfPsrLogAwareTrait */ protected $logger; - public function setLogger(LoggerInterface $logger): void + public function setLogger(LoggerInterface $logger) { $this->logger = $logger; if (property_exists($this, 'services') && is_array($this->services)) { diff --git a/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php b/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php index a0fcb3a5..e2db69c3 100644 --- a/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php +++ b/vendor/mpdf/psr-log-aware-trait/src/PsrLogAwareTrait.php @@ -12,7 +12,7 @@ trait PsrLogAwareTrait */ protected $logger; - public function setLogger(LoggerInterface $logger): void + public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json index 879fc6f5..ca056953 100644 --- a/vendor/psr/log/composer.json +++ b/vendor/psr/log/composer.json @@ -11,16 +11,16 @@ } ], "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } } } diff --git a/vendor/psr/log/src/AbstractLogger.php b/vendor/psr/log/src/AbstractLogger.php deleted file mode 100644 index d60a091a..00000000 --- a/vendor/psr/log/src/AbstractLogger.php +++ /dev/null @@ -1,15 +0,0 @@ -logger = $logger; - } -} diff --git a/vendor/psr/log/src/LoggerInterface.php b/vendor/psr/log/src/LoggerInterface.php deleted file mode 100644 index cb4cf648..00000000 --- a/vendor/psr/log/src/LoggerInterface.php +++ /dev/null @@ -1,98 +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. - */ - public function alert(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - */ - public function critical(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - */ - public function error(string|\Stringable $message, array $context = []): void - { - $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. - */ - public function warning(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * Normal but significant events. - */ - public function notice(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - */ - public function info(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * Detailed debug information. - */ - public function debug(string|\Stringable $message, array $context = []): void - { - $this->log(LogLevel::DEBUG, $message, $context); - } - - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * - * @throws \Psr\Log\InvalidArgumentException - */ - abstract public function log($level, string|\Stringable $message, array $context = []): void; -} diff --git a/vendor/psr/log/src/NullLogger.php b/vendor/psr/log/src/NullLogger.php deleted file mode 100644 index de0561e2..00000000 --- a/vendor/psr/log/src/NullLogger.php +++ /dev/null @@ -1,26 +0,0 @@ -logger) { }` - * blocks. - */ -class NullLogger extends AbstractLogger -{ - /** - * Logs with an arbitrary level. - * - * @param mixed[] $context - * - * @throws \Psr\Log\InvalidArgumentException - */ - public function log($level, string|\Stringable $message, array $context = []): void - { - // noop - } -}